MessageExtractor::getExtractor()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 9
ccs 0
cts 0
cp 0
rs 10
cc 2
nc 2
nop 1
crap 6
1
<?php
2
3
namespace Printful\GettextCms;
4
5
use Gettext\Extractors\ExtractorInterface;
6
use Gettext\Extractors\ExtractorMultiInterface;
7
use Gettext\Extractors\JsCode;
8
use Gettext\Extractors\PhpCode;
9
use Gettext\Extractors\VueJs;
10
use Gettext\Translations;
11
use League\Flysystem\Adapter\Local;
12
use League\Flysystem\Filesystem;
13
use League\Flysystem\Plugin\ListFiles;
14
use Printful\GettextCms\Exceptions\GettextCmsException;
15
use Printful\GettextCms\Exceptions\InvalidPathException;
16
use Printful\GettextCms\Exceptions\UnknownExtractorException;
17
use Printful\GettextCms\Interfaces\MessageConfigInterface;
18
use Printful\GettextCms\Structures\ScanItem;
19
20
/**
21
 * Class extracts gettext function calls from source files and converts them to Translation objects
22
 */
23
class MessageExtractor
24
{
25
    const EXTRACTORS = [
26
        'js' => JsCode::class,
27
        'vue' => VueJs::class,
28
        'php' => PhpCode::class,
29
    ];
30
31
    /** @var MessageConfigInterface */
32
    private $config;
33
34
    /**
35
     * @param MessageConfigInterface $config
36 17
     */
37
    public function __construct(MessageConfigInterface $config)
38 17
    {
39 17
        $this->config = $config;
40
    }
41
42
    /**
43
     * @param ScanItem[] $items
44
     * @param array|null $domains Force domains to scan. If null, will scan default domains.
45
     * @return Translations[] List of translations for each domain
46
     * @throws GettextCmsException
47 11
     */
48
    public function extract(array $items, array $domains = null): array
49 11
    {
50
        $defaultDomain = $this->config->getDefaultDomain();
51 11
52 9
        if (!$domains) {
53 9
            $domains = $this->config->getOtherDomains();
54
            $domains[] = $defaultDomain;
55
        }
56 11
57
        /** @var Translations[] $allTranslations [domain => translations, ...] */
58 11
        $allTranslations = array_reduce($domains, function (&$carry, string $domain) use ($defaultDomain) {
59 11
            $translations = new Translations();
60
61
            // When we scan for default domain, we have to specify an empty value
62
            // otherwise we would search for domain function calls with this domain
63
            // For example, empty domain will find string like __("message")
64
            // But if a domain is specified, it will look for dgettext("custom-domain", "message")
65 11
            if ($domain !== $defaultDomain) {
66 6
                $translations->setDomain($domain);
67
            }
68
69 11
            $carry[$domain] = $translations;
70
71 11
            return $carry;
72
        }, []);
73
74
        foreach ($items as $item) {
75 10
            // Scan for this item, translations will be merged with all domain translations
76
            $this->extractForDomains($item, array_values($allTranslations));
77 10
        }
78
79
        // Always set the domain even if it is the default one
80 10
        // This is needed because default domain won't be set for the translations instance
81
        foreach ($allTranslations as $domain => $translations) {
82
            $translations->setDomain($domain);
83
        }
84
85
        return array_values($allTranslations);
86
    }
87
88
    /**
89
     * @param ScanItem $scanItem
90 11
     * @param Translations[] $translations
91
     * @return Translations[]
92 11
     * @throws InvalidPathException
93
     * @throws UnknownExtractorException
94 11
     */
95
    private function extractForDomains(ScanItem $scanItem, array $translations): array
96 11
    {
97
        $pathnames = $this->resolvePathnames($scanItem);
98
99 10
        foreach ($pathnames as $pathname) {
100
            // Translations will be merged with given object
101
            $this->extractFileMessages($pathname, $translations, $scanItem->functions);
102
        }
103
104
        return $translations;
105
    }
106
107
    /**
108 11
     * @param $pathname
109
     * @param Translations[] $translations
110 11
     * @param array|null $functions Optional functions to scan for
111
     * @throws UnknownExtractorException
112
     */
113 10
    private function extractFileMessages($pathname, array $translations, array $functions = null)
114 10
    {
115
        $extractor = $this->getExtractor($pathname);
116
117 10
        $options = [
118 2
            'extractComments' => '', // This extracts comments above function call
119
            // HTML attribute prefixes we parse as JS which could contain translations (are JS expressions)
120
            'attributePrefixes' => [
121 10
                ':',
122 10
                'v-',
123
            ],
124
        ];
125
126
        if ($functions) {
127
            $options['functions'] = $functions;
128
        }
129
130
        $extractor::fromFileMultiple($pathname, $translations, $options);
131 16
    }
132
133 16
    /**
134 12
     * Returns a list of files that match this item (if single file, then an array with a single pathname)
135
     *
136
     * @param ScanItem $item
137 4
     * @return array List of matching pathnames
138 1
     * @throws InvalidPathException
139
     */
140
    public function resolvePathnames(ScanItem $item): array
141 3
    {
142
        if (is_file($item->path)) {
143
            return [$item->path];
144
        }
145
146
        if (!is_dir($item->path)) {
147
            throw new InvalidPathException('Path "' . $item->path . '" does not exist');
148
        }
149
150 3
        return $this->resolveDirectoryFiles($item);
151
    }
152 3
153
    /**
154 3
     * If scan item is for a directory, this will create a list of matching files
155 3
     *
156 3
     * @param ScanItem $item
157
     * @return string[] List of pathnames to files
158 3
     */
159
    private function resolveDirectoryFiles(ScanItem $item): array
160
    {
161 3
        $dir = realpath($item->path);
162
163
        $adapter = new Local($dir);
164 3
        $filesystem = new Filesystem($adapter);
165 3
        $filesystem->addPlugin(new ListFiles());
166 3
167
        $files = $filesystem->listFiles('', $item->recursive);
168 3
169 3
        // If no extensions are given, fallback to known extensions
170 3
        $extensions = $item->extensions ?: array_keys(self::EXTRACTORS);
171
172
        // If extensions are set, filter other files out
173
        $files = array_filter($files, function ($file) use ($item, $extensions) {
0 ignored issues
show
Unused Code introduced by
The import $item is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
174
            return isset($file['extension']) && in_array($file['extension'], $extensions);
175
        });
176
177
        return array_map(function ($file) use ($dir) {
178 11
            return $dir . DIRECTORY_SEPARATOR . $file['path'];
179
        }, $files);
180 11
    }
181
182 11
    /**
183 10
     * @param string $pathname Full path to file
184
     * @return ExtractorInterface|ExtractorMultiInterface|string Name of the extraction class for the given file
185
     * @throws UnknownExtractorException
186 1
     */
187
    private function getExtractor($pathname): string
188
    {
189
        $extension = pathinfo($pathname, PATHINFO_EXTENSION);
190
191
        if (isset(self::EXTRACTORS[$extension])) {
192
            return self::EXTRACTORS[$extension];
193
        }
194
195
        throw new UnknownExtractorException('Extractor is not know for file extension "' . $extension . '"');
0 ignored issues
show
Bug introduced by
Are you sure $extension of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

195
        throw new UnknownExtractorException('Extractor is not know for file extension "' . /** @scrutinizer ignore-type */ $extension . '"');
Loading history...
196
    }
197
}