Completed
Push — master ( 097407...fee58a )
by Mārtiņš
01:54
created

MessageExtractor::extractForDomains()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 10
ccs 2
cts 2
cp 1
rs 10
cc 2
nc 2
nop 2
crap 2
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
        ];
120
121 10
        if ($functions) {
122 10
            $options['functions'] = $functions;
123
        }
124
125
        $extractor::fromFileMultiple($pathname, $translations, $options);
126
    }
127
128
    /**
129
     * Returns a list of files that match this item (if single file, then an array with a single pathname)
130
     *
131 16
     * @param ScanItem $item
132
     * @return array List of matching pathnames
133 16
     * @throws InvalidPathException
134 12
     */
135
    public function resolvePathnames(ScanItem $item): array
136
    {
137 4
        if (is_file($item->path)) {
138 1
            return [$item->path];
139
        }
140
141 3
        if (!is_dir($item->path)) {
142
            throw new InvalidPathException('Path "' . $item->path . '" does not exist');
143
        }
144
145
        return $this->resolveDirectoryFiles($item);
146
    }
147
148
    /**
149
     * If scan item is for a directory, this will create a list of matching files
150 3
     *
151
     * @param ScanItem $item
152 3
     * @return string[] List of pathnames to files
153
     */
154 3
    private function resolveDirectoryFiles(ScanItem $item): array
155 3
    {
156 3
        $dir = realpath($item->path);
157
158 3
        $adapter = new Local($dir);
159
        $filesystem = new Filesystem($adapter);
160
        $filesystem->addPlugin(new ListFiles);
161 3
162
        $files = $filesystem->listFiles('', $item->recursive);
163
164 3
        // If no extensions are given, fallback to known extensions
165 3
        $extensions = $item->extensions ?: array_keys(self::EXTRACTORS);
166 3
167
        // If extensions are set, filter other files out
168 3
        $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...
169 3
            return in_array($file['extension'], $extensions);
170 3
        });
171
172
        return array_map(function ($file) use ($dir) {
173
            return $dir . DIRECTORY_SEPARATOR . $file['path'];
174
        }, $files);
175
    }
176
177
    /**
178 11
     * @param string $pathname Full path to file
179
     * @return ExtractorInterface|ExtractorMultiInterface|string Name of the extraction class for the given file
180 11
     * @throws UnknownExtractorException
181
     */
182 11
    private function getExtractor($pathname): string
183 10
    {
184
        $extension = pathinfo($pathname, PATHINFO_EXTENSION);
185
186 1
        if (isset(self::EXTRACTORS[$extension])) {
187
            return self::EXTRACTORS[$extension];
188
        }
189
190
        throw new UnknownExtractorException('Extractor is not know for file extension "' . $extension . '"');
191
    }
192
}