Completed
Push — master ( f96c7c...8d3ead )
by Mathieu
02:17 queued 10s
created

Importer::convertSourceLocationsToMessages()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 0
cts 22
cp 0
rs 9.1928
c 0
b 0
f 0
cc 5
nc 6
nop 2
crap 30
1
<?php
2
3
/*
4
 * This file is part of the PHP Translation package.
5
 *
6
 * (c) PHP Translation team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Translation\Bundle\Service;
13
14
use Symfony\Component\Finder\Finder;
15
use Symfony\Component\Translation\MessageCatalogue;
16
use Translation\Bundle\Catalogue\Operation\ReplaceOperation;
17
use Translation\Bundle\Model\ImportResult;
18
use Translation\Bundle\Model\Metadata;
19
use Translation\Bundle\Twig\Visitor\DefaultApplyingNodeVisitor;
20
use Translation\Bundle\Twig\Visitor\RemovingNodeVisitor;
21
use Translation\Extractor\Extractor;
22
use Translation\Extractor\Model\SourceCollection;
23
use Translation\Extractor\Model\SourceLocation;
24
use Twig\Environment;
25
26
/**
27
 * Use extractors to import translations to message catalogues.
28
 *
29
 * @author Tobias Nyholm <[email protected]>
30
 */
31
final class Importer
32
{
33
    /**
34
     * @var Extractor
35
     */
36
    private $extractor;
37
38
    /**
39
     * @var array
40
     */
41
    private $config;
42
43
    /**
44
     * @var Environment
45
     */
46
    private $twig;
47
48
    /**
49
     * @var string
50
     */
51
    private $defaultLocale;
52
53
    public function __construct(Extractor $extractor, Environment $twig, string $defaultLocale)
54
    {
55
        $this->extractor = $extractor;
56
        $this->twig = $twig;
57
        $this->defaultLocale = $defaultLocale;
58
    }
59
60
    /**
61
     * @param MessageCatalogue[] $catalogues
62
     * @param array              $config     {
63
     *
64
     *     @var array $blacklist_domains Blacklist the domains we should exclude. Cannot be used with whitelist.
65
     *     @var array $whitelist_domains Whitelist the domains we should include. Cannot be used with blacklist.
66
     *     @var string $project_root The project root will be removed from the source location.
67
     * }
68
     */
69
    public function extractToCatalogues(Finder $finder, array $catalogues, array $config = []): ImportResult
70
    {
71
        $this->processConfig($config);
72
        $this->disableTwigVisitors();
73
        $sourceCollection = $this->extractor->extract($finder);
74
        $results = [];
75
        foreach ($catalogues as $catalogue) {
76
            $target = new MessageCatalogue($catalogue->getLocale());
77
            $this->convertSourceLocationsToMessages($target, $sourceCollection);
78
79
            // Remove all SourceLocation and State form catalogue.
80
            foreach ($catalogue->getDomains() as $domain) {
81
                foreach ($catalogue->all($domain) as $key => $translation) {
82
                    $meta = $this->getMetadata($catalogue, $key, $domain);
83
                    $meta->removeAllInCategory('file-source');
84
                    $meta->removeAllInCategory('state');
85
                    $this->setMetadata($catalogue, $key, $domain, $meta);
86
                }
87
            }
88
89
            $merge = new ReplaceOperation($target, $catalogue);
90
            $result = $merge->getResult();
91
            $domains = $merge->getDomains();
92
93
            // Mark new messages as new/obsolete
94
            foreach ($domains as $domain) {
95
                foreach ($merge->getNewMessages($domain) as $key => $translation) {
96
                    $meta = $this->getMetadata($result, $key, $domain);
0 ignored issues
show
Compatibility introduced by
$result of type object<Symfony\Component...sageCatalogueInterface> is not a sub-type of object<Symfony\Component...ation\MessageCatalogue>. It seems like you assume a concrete implementation of the interface Symfony\Component\Transl...ssageCatalogueInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
97
                    $meta->setState('new');
98
                    $this->setMetadata($result, $key, $domain, $meta);
0 ignored issues
show
Compatibility introduced by
$result of type object<Symfony\Component...sageCatalogueInterface> is not a sub-type of object<Symfony\Component...ation\MessageCatalogue>. It seems like you assume a concrete implementation of the interface Symfony\Component\Transl...ssageCatalogueInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
99
100
                    // Add custom translations that we found in the source
101
                    if (null === $translation) {
102
                        if (null !== $newTranslation = $meta->getTranslation()) {
103
                            $result->set($key, $newTranslation, $domain);
104
                            // We do not want "translation" key stored anywhere.
105
                            $meta->removeAllInCategory('translation');
106
                        } elseif (null !== ($newTranslation = $meta->getDesc()) && $catalogue->getLocale() === $this->defaultLocale) {
107
                            $result->set($key, $newTranslation, $domain);
108
                        }
109
                    }
110
                }
111
                foreach ($merge->getObsoleteMessages($domain) as $key => $translation) {
112
                    $meta = $this->getMetadata($result, $key, $domain);
0 ignored issues
show
Compatibility introduced by
$result of type object<Symfony\Component...sageCatalogueInterface> is not a sub-type of object<Symfony\Component...ation\MessageCatalogue>. It seems like you assume a concrete implementation of the interface Symfony\Component\Transl...ssageCatalogueInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
113
                    $meta->setState('obsolete');
114
                    $this->setMetadata($result, $key, $domain, $meta);
0 ignored issues
show
Compatibility introduced by
$result of type object<Symfony\Component...sageCatalogueInterface> is not a sub-type of object<Symfony\Component...ation\MessageCatalogue>. It seems like you assume a concrete implementation of the interface Symfony\Component\Transl...ssageCatalogueInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
115
                }
116
            }
117
            $results[] = $result;
118
        }
119
120
        return new ImportResult($results, $sourceCollection->getErrors());
121
    }
122
123
    private function convertSourceLocationsToMessages(MessageCatalogue $catalogue, SourceCollection $collection): void
124
    {
125
        /** @var SourceLocation $sourceLocation */
126
        foreach ($collection as $sourceLocation) {
127
            $context = $sourceLocation->getContext();
128
            $domain = $context['domain'] ?? 'messages';
129
            // Check with white/black list
130
            if (!$this->isValidDomain($domain)) {
131
                continue;
132
            }
133
134
            $key = $sourceLocation->getMessage();
135
            $catalogue->set($key, null, $domain);
136
            $trimLength = 1 + \strlen($this->config['project_root']);
137
138
            $meta = $this->getMetadata($catalogue, $key, $domain);
139
            $meta->addCategory('file-source', \sprintf('%s:%s', \substr($sourceLocation->getPath(), $trimLength), $sourceLocation->getLine()));
140
            if (isset($sourceLocation->getContext()['desc'])) {
141
                $meta->addCategory('desc', $sourceLocation->getContext()['desc']);
142
            }
143
            if (isset($sourceLocation->getContext()['translation'])) {
144
                $meta->addCategory('translation', $sourceLocation->getContext()['translation']);
145
            }
146
            $this->setMetadata($catalogue, $key, $domain, $meta);
147
        }
148
    }
149
150
    private function getMetadata(MessageCatalogue $catalogue, string $key, string $domain): Metadata
151
    {
152
        return new Metadata($catalogue->getMetadata($key, $domain));
153
    }
154
155
    private function setMetadata(MessageCatalogue $catalogue, string $key, string $domain, Metadata $metadata): void
156
    {
157
        $catalogue->setMetadata($key, $metadata->toArray(), $domain);
158
    }
159
160
    private function isValidDomain(string $domain): bool
161
    {
162 View Code Duplication
        if (!empty($this->config['blacklist_domains']) && \in_array($domain, $this->config['blacklist_domains'], true)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
163
            return false;
164
        }
165 View Code Duplication
        if (!empty($this->config['whitelist_domains']) && !\in_array($domain, $this->config['whitelist_domains'], true)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
166
            return false;
167
        }
168
169
        return true;
170
    }
171
172
    /**
173
     * Make sure the configuration is valid.
174
     */
175
    private function processConfig(array $config): void
176
    {
177
        $default = [
178
            'project_root' => '',
179
            'blacklist_domains' => [],
180
            'whitelist_domains' => [],
181
        ];
182
183
        $config = \array_merge($default, $config);
184
185 View Code Duplication
        if (!empty($config['blacklist_domains']) && !empty($config['whitelist_domains'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
186
            throw new \InvalidArgumentException('Cannot use "blacklist_domains" and "whitelist_domains" at the same time');
187
        }
188
189 View Code Duplication
        if (!empty($config['blacklist_domains']) && !\is_array($config['blacklist_domains'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
190
            throw new \InvalidArgumentException('Config parameter "blacklist_domains" must be an array');
191
        }
192
193 View Code Duplication
        if (!empty($config['whitelist_domains']) && !\is_array($config['whitelist_domains'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
194
            throw new \InvalidArgumentException('Config parameter "whitelist_domains" must be an array');
195
        }
196
197
        $this->config = $config;
198
    }
199
200
    private function disableTwigVisitors(): void
201
    {
202
        foreach ($this->twig->getNodeVisitors() as $visitor) {
203
            if ($visitor instanceof DefaultApplyingNodeVisitor) {
204
                $visitor->setEnabled(false);
205
            }
206
            if ($visitor instanceof RemovingNodeVisitor) {
207
                $visitor->setEnabled(false);
208
            }
209
        }
210
    }
211
}
212