Passed
Push — master ( 11c846...7b33d0 )
by Mārtiņš
01:53
created

MessageStorage::setAllAsNotDynamic()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
namespace Printful\GettextCms;
4
5
use Gettext\Languages\Language;
6
use Gettext\Translation;
7
use Gettext\Translations;
8
use Printful\GettextCms\Exceptions\InvalidTranslationException;
9
use Printful\GettextCms\Interfaces\MessageRepositoryInterface;
10
use Printful\GettextCms\Structures\MessageItem;
11
12
/**
13
 * Class handles all saving and retrieving logic of translations using the given repository
14
 */
15
class MessageStorage
16
{
17
    private const TYPE_FILE = 'file';
18
    private const TYPE_DYNAMIC = 'dynamic';
19
20
    /** @var MessageRepositoryInterface */
21
    private $repository;
22
23
    /**
24
     * Cache for plural form counts
25
     * @var array [locale => plural count, ..]
26
     */
27
    private $pluralFormCache = [];
28
29 42
    public function __construct(MessageRepositoryInterface $repository)
30
    {
31 42
        $this->repository = $repository;
32 42
    }
33
34
    /**
35
     * Save translation object for a single domain and locale
36
     *
37
     * @param Translations $translations
38
     * @throws InvalidTranslationException
39
     */
40 13
    public function createOrUpdate(Translations $translations)
41
    {
42 13
        $this->validateHeaders($translations);
43
44 11
        $locale = $translations->getLanguage();
45 11
        $domain = $translations->getDomain();
46
47 11
        foreach ($translations as $translation) {
48 11
            $this->createOrUpdateSingle($locale, $domain, $translation);
49
        }
50 11
    }
51
52
    /**
53
     * @param Translations $translations
54
     * @throws InvalidTranslationException
55
     */
56 13
    private function validateHeaders(Translations $translations)
57
    {
58 13
        if (!$translations->getLanguage()) {
59 1
            throw new InvalidTranslationException('Locale is missing');
60
        }
61
62 12
        if (!$translations->getDomain()) {
63 1
            throw new InvalidTranslationException('Domain is missing');
64
        }
65 11
    }
66
67
    /**
68
     * Create or update a translation that is found in a file
69
     *
70
     * @param string $locale
71
     * @param string $domain
72
     * @param Translation $translation
73
     * @return bool
74
     */
75 29
    public function createOrUpdateSingle(string $locale, string $domain, Translation $translation): bool
76
    {
77 29
        return $this->createOrUpdateSingleWithType($locale, $domain, $translation, self::TYPE_FILE);
78
    }
79
80
    /**
81
     * Create or update a translation that is dynamically generated
82
     *
83
     * @param string $locale
84
     * @param string $domain
85
     * @param Translation $translation
86
     * @return bool
87
     */
88 6
    public function createOrUpdateSingleDynamic(string $locale, string $domain, Translation $translation): bool
89
    {
90 6
        return $this->createOrUpdateSingleWithType($locale, $domain, $translation, self::TYPE_DYNAMIC);
91
    }
92
93
    /**
94
     * Save a translation to repository by merging it to a previously saved version.
95
     *
96
     * @param string $locale
97
     * @param string $domain
98
     * @param Translation $translation
99
     * @param string $type
100
     * @return bool
101
     */
102 31
    private function createOrUpdateSingleWithType(
103
        string $locale,
104
        string $domain,
105
        Translation $translation,
106
        string $type
107
    ): bool {
108
        // Make a clone so we don't modify the passed instance
109 31
        $translation = $translation->getClone();
110
111 31
        $key = $this->getKey($locale, $domain, $translation);
112
113 31
        $existingItem = $this->repository->getSingle($key);
114
115 31
        if ($existingItem->exists()) {
116 9
            $existingTranslation = $this->itemToTranslation($existingItem);
117 9
            $translation->mergeWith($existingTranslation);
118 9
            $item = $this->translationToItem($locale, $domain, $translation);
119
        } else {
120 31
            $item = $this->translationToItem($locale, $domain, $translation);
121
        }
122
123 31
        $item->isInJs = $item->isInJs ?: $existingItem->isInJs;
124
125 31
        if ($type == self::TYPE_DYNAMIC || $existingItem->isDynamic) {
126 6
            $item->isDynamic = true;
127
        }
128
129 31
        if ($type == self::TYPE_FILE || $item->isInJs || $existingItem->isInFile) {
130 29
            $item->isInFile = true;
131
        }
132
133 31
        $item->hasOriginalTranslation = $translation->hasTranslation();
134 31
        $item->requiresTranslating = $this->requiresTranslating($locale, $translation);
135
136 31
        $item->isDisabled = $item->isDisabled ?: (!$item->isInJs && !$item->isInFile && !$item->isDynamic);
137
138 31
        if (!$this->hasChanged($existingItem, $item)) {
139
            return true;
140
        }
141
142 31
        return $this->repository->save($item);
143
    }
144
145
    /**
146
     * Save translation object for a single domain and locale
147
     *
148
     * @param Translations $translations
149
     * @throws InvalidTranslationException
150
     */
151 2
    public function saveTranslated(Translations $translations)
152
    {
153 2
        $this->validateHeaders($translations);
154
155 2
        $locale = $translations->getLanguage();
156 2
        $domain = $translations->getDomain();
157
158 2
        foreach ($translations as $translation) {
159 2
            $this->saveTranslatedSingle($locale, $domain, $translation);
160
        }
161 2
    }
162
163
    /**
164
     * Function for saving only translated values.
165
     * This will not modify disabled state and will not create new entries in repository, only modifies existing
166
     *
167
     * @param string $locale
168
     * @param string $domain
169
     * @param Translation $translation
170
     * @return bool
171
     */
172 5
    public function saveTranslatedSingle(string $locale, string $domain, Translation $translation): bool
173
    {
174 5
        if (!$translation->hasTranslation()) {
175 1
            return false;
176
        }
177
178 4
        $existingItem = $this->repository->getSingle($this->getKey($locale, $domain, $translation));
179
180 4
        if (!$existingItem->exists()) {
181 1
            return false;
182
        }
183
184 3
        $existingTranslation = $this->itemToTranslation($existingItem);
185
186 3
        $existingTranslation->setTranslation($translation->getTranslation());
187
188
        // Make sure we do not drop previous plural translations if current one does not contain one
189 3
        $pluralTranslations = $translation->getPluralTranslations();
190
191 3
        if (!empty($pluralTranslations)) {
192 2
            $existingTranslation->setPluralTranslations($pluralTranslations);
193
        }
194
195 3
        $item = $this->translationToItem($locale, $domain, $existingTranslation);
196
197 3
        $item->hasOriginalTranslation = true;
198
        // It's still possible that plurals are missing and this translation still needs work
199 3
        $item->requiresTranslating = $this->requiresTranslating($locale, $translation);
200
201 3
        if (!$this->hasChanged($existingItem, $item)) {
202
            return true;
203
        }
204
205 3
        return $this->repository->save($item);
206
    }
207
208
    /**
209
     * Check if some translations are missing (original or missing plural forms)
210
     *
211
     * @param string $locale
212
     * @param Translation $translation
213
     * @return bool
214
     */
215 31
    private function requiresTranslating(string $locale, Translation $translation): bool
216
    {
217 31
        if (!$translation->hasTranslation()) {
218 13
            return true;
219
        }
220
221 23
        if ($translation->hasPlural()) {
222 8
            $translatedPluralCount = count(array_filter($translation->getPluralTranslations()));
223
            // If there are less plural translations than language requires, this needs translating
224 8
            return $this->getPluralCount($locale) !== $translatedPluralCount;
225
        }
226
227 17
        return false;
228
    }
229
230
    /**
231
     * Get number of plural forms for this locale
232
     *
233
     * @param string $locale
234
     * @return int
235
     * @throws \InvalidArgumentException Thrown in the locale is not correct
236
     */
237 8
    private function getPluralCount(string $locale): int
238
    {
239 8
        if (!array_key_exists($locale, $this->pluralFormCache)) {
240 8
            $info = Language::getById($locale);
241
242
            // Minus one, because we do not count the original string as a plural
243 8
            $this->pluralFormCache[$locale] = count($info->categories) - 1;
244
        }
245
246 8
        return $this->pluralFormCache[$locale];
247
    }
248
249
    /**
250
     * Generate a unique key for storage (basically a primary key)
251
     *
252
     * @param string $locale
253
     * @param string $domain
254
     * @param Translation $translation
255
     * @return string
256
     */
257 32
    private function getKey(string $locale, string $domain, Translation $translation): string
258
    {
259 32
        return md5($locale . '|' . $domain . '|' . $translation->getContext() . '|' . $translation->getOriginal());
260
    }
261
262
    /**
263
     * Convert a message item to a translation item
264
     *
265
     * @param MessageItem $item
266
     * @return Translation
267
     */
268 27
    private function itemToTranslation(MessageItem $item): Translation
269
    {
270 27
        $translation = new Translation($item->context, $item->original, $item->originalPlural);
271
272 27
        $translation->setTranslation($item->translation);
273 27
        $translation->setPluralTranslations($item->pluralTranslations);
274 27
        $translation->setDisabled($item->isDisabled);
275
276 27
        foreach ($item->references as $v) {
277 7
            $translation->addReference($v[0], $v[1]);
278
        }
279
280 27
        foreach ($item->comments as $v) {
281 1
            $translation->addComment($v);
282
        }
283
284 27
        foreach ($item->extractedComments as $v) {
285 1
            $translation->addExtractedComment($v);
286
        }
287
288 27
        return $translation;
289
    }
290
291
    /**
292
     * Convert a translation item to a message item
293
     *
294
     * @param string $locale
295
     * @param string $domain
296
     * @param Translation $translation
297
     * @return MessageItem
298
     */
299 31
    private function translationToItem(string $locale, string $domain, Translation $translation): MessageItem
300
    {
301 31
        $item = new MessageItem;
302
303 31
        $item->key = $this->getKey($locale, $domain, $translation);
304 31
        $item->domain = $domain;
305 31
        $item->locale = $locale;
306 31
        $item->context = (string)$translation->getContext();
307 31
        $item->original = $translation->getOriginal();
308 31
        $item->translation = $translation->getTranslation();
309 31
        $item->originalPlural = $translation->getPlural();
310 31
        $item->pluralTranslations = $translation->getPluralTranslations();
311 31
        $item->references = $translation->getReferences();
312 31
        $item->comments = $translation->getComments();
313 31
        $item->extractedComments = $translation->getExtractedComments();
314 31
        $item->isDisabled = $translation->isDisabled();
315
316 31
        foreach ($item->references as $reference) {
317 7
            if ($this->isJsFile($reference[0] ?? '')) {
318 2
                $item->isInJs = true;
319 7
                break;
320
            }
321
        }
322
323 31
        return $item;
324
    }
325
326
    /**
327
     * Check if this is considered a JS file.
328
     *
329
     * @param string $filename
330
     * @return bool
331
     */
332 7
    private function isJsFile(string $filename): bool
333
    {
334 7
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
335
336 7
        return in_array($extension, ['vue', 'js'], true);
337
    }
338
339
    /**
340
     * All translations, including disabled, enabled and untranslated
341
     *
342
     * @param string $locale
343
     * @param $domain
344
     * @return Translations
345
     */
346 6
    public function getAll(string $locale, $domain): Translations
347
    {
348 6
        return $this->convertItems(
349 6
            $locale,
350 6
            (string)$domain,
351 6
            $this->repository->getAll($locale, (string)$domain)
352
        );
353
    }
354
355
    /**
356
     * All enabled translations including untranslated
357
     *
358
     * @param string $locale
359
     * @param $domain
360
     * @return Translations
361
     */
362 6
    public function getAllEnabled(string $locale, $domain): Translations
363
    {
364 6
        return $this->convertItems(
365 6
            $locale,
366 6
            (string)$domain,
367 6
            $this->repository->getEnabled($locale, (string)$domain)
368
        );
369
    }
370
371
    /**
372
     * Enabled and translated translations only
373
     *
374
     * @param string $locale
375
     * @param $domain
376
     * @return Translations
377
     */
378 12
    public function getEnabledTranslated(string $locale, $domain): Translations
379
    {
380 12
        $items = $this->repository->getEnabledTranslated($locale, (string)$domain);
381
382 12
        return $this->convertItems($locale, (string)$domain, $items);
383
    }
384
385
    /**
386
     * Enabled and translated JS translations
387
     *
388
     * @param string $locale
389
     * @param $domain
390
     * @return Translations
391
     */
392 2
    public function getEnabledTranslatedJs(string $locale, $domain): Translations
393
    {
394 2
        $items = $this->repository->getEnabledTranslatedJs($locale, (string)$domain);
395
396 2
        return $this->convertItems($locale, (string)$domain, $items);
397
    }
398
399
    /**
400
     * Enabled and translated translations only
401
     *
402
     * @param string $locale
403
     * @param $domain
404
     * @return Translations
405
     */
406 9
    public function getRequiresTranslating(string $locale, $domain): Translations
407
    {
408 9
        return $this->convertItems(
409 9
            $locale,
410 9
            (string)$domain,
411 9
            $this->repository->getRequiresTranslating($locale, (string)$domain)
412
        );
413
    }
414
415
    /**
416
     * Converts message items to a translation object
417
     *
418
     * @param string $locale
419
     * @param string|null $domain
420
     * @param MessageItem[] $items
421
     * @return Translations
422
     */
423 30
    private function convertItems(string $locale, $domain, array $items): Translations
424
    {
425 30
        $domain = (string)$domain;
426
427 30
        $translations = new Translations;
428 30
        $translations->setDomain($domain);
429 30
        $translations->setLanguage($locale);
430
431 30
        foreach ($items as $v) {
432 26
            $translation = $this->itemToTranslation($v);
433 26
            $translations[] = $translation;
434
        }
435
436 30
        return $translations;
437
    }
438
439
    /**
440
     * Check if new item is different than the old one
441
     *
442
     * @param MessageItem $old
443
     * @param MessageItem $new
444
     * @return bool
445
     */
446 31
    private function hasChanged(MessageItem $old, MessageItem $new): bool
0 ignored issues
show
Unused Code introduced by
The parameter $old is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

446
    private function hasChanged(/** @scrutinizer ignore-unused */ MessageItem $old, MessageItem $new): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $new is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

446
    private function hasChanged(MessageItem $old, /** @scrutinizer ignore-unused */ MessageItem $new): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
447
    {
448
        // TODO implement
449 31
        return true;
450
    }
451
452
    /**
453
     * Set all locale and domain messages as not present in files ("isFile" and "isInJs" fields should be set to false)
454
     *
455
     * @param string $locale
456
     * @param string $domain
457
     */
458 4
    public function setAllAsNotInFilesAndInJs(string $locale, string $domain)
459
    {
460 4
        $this->repository->setAllAsNotInFilesAndInJs($locale, $domain);
461 4
    }
462
463
    /**
464
     * Set all locale and domain messages as not dynamic ("isDynamic" field should be set to false)
465
     *
466
     * @param string $locale
467
     * @param string $domain
468
     */
469 6
    public function setAllAsNotDynamic(string $locale, string $domain)
470
    {
471 6
        $this->repository->setAllAsNotDynamic($locale, $domain);
472 6
    }
473
474
    /**
475
     * Set all messages as disabled which are not used (messages which filed "isDynamic", "isInFile" and "isInJs" all are false)
476
     */
477 7
    public function disableUnused()
478
    {
479 7
        $this->repository->disableUnused();
480
    }
481
}