Extractor::isSupportedFile()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 1
1
<?php
2
3
/**
4
 * @package Extractor
5
 * @author Iurii Makukh <[email protected]>
6
 * @copyright Copyright (c) 2015, Iurii Makukh
7
 * @license https://www.gnu.org/licenses/gpl.html GNU/GPLv3
8
 */
9
10
namespace gplcart\modules\extractor\models;
11
12
use DirectoryIterator;
13
use gplcart\core\Hook;
14
use gplcart\core\Module;
15
16
/**
17
 * Methods to extract translatable strings from various source files
18
 */
19
class Extractor
20
{
21
22
    /**
23
     * Hook class instance
24
     * @var \gplcart\core\Hook $hook
25
     */
26
    protected $hook;
27
28
    /**
29
     * Module class instance
30
     * @var \gplcart\core\Module $module
31
     */
32
    protected $module;
33
34
    /**
35
     * Max parsing width in columns
36
     */
37
    const MAX_COLS = 500;
38
39
    /**
40
     * Max rows to parse per file
41
     */
42
    const MAX_LINES = 5000;
43
44
    /**
45
     * Pattern to extract strings from JS function Gplcart.text()
46
     */
47
    const PATTERN_JS = '/Gplcart.text\s*\(\s*([\'"])(.+?)\1\s*([\),])/s';
48
49
    /**
50
     * Pattern to extract strings from TWIG function {{ text() }}
51
     */
52
    const PATTERN_TWIG = '/text\s*\(\s*([\'"])(.+?)\1\s*([\),])/s';
53
54
    /**
55
     * Pattern to extract strings using inline @text annotation that goes before the translatable staring
56
     */
57
    const PATTERN_ANNOTATION_BEFORE = '/\/\*(?:\*|\s*)@text(?:\*|\s*)\*\/\s*([\'"])(.+?)\1\s*/';
58
59
    /**
60
     * Pattern to extract strings using inline // @text annotation that goes after the translatable staring
61
     */
62
    const PATTERN_ANNOTATION_AFTER = '/([\'"])((?:(?!\1).)+)\1(?:\s*)(?:;*|,*)\s*\/\/\s*@text\s*$/';
63
64
    /**
65
     * Pattern to extract strings from Language::text() method
66
     */
67
    const PATTERN_PHP_METHOD = '/->text\(\s*([\'"])(.+?)\1\s*([\),])/s';
68
69
    /**
70
     * Pattern to extract strings from gplcart_text() function
71
     */
72
    const PATTERN_PHP_FUNCTION = '/gplcart_text\(\s*([\'"])(.+?)\1\s*([\),])/s';
73
74
    /**
75
     * @param Hook $hook
76
     * @param Module $module
77
     */
78
    public function __construct(Hook $hook, Module $module)
79
    {
80
        $this->hook = $hook;
81
        $this->module = $module;
82
    }
83
84
    /**
85
     * Returns an extractor by a file extension or an array of extractors keyed by extension
86
     * @param null|string $extension
87
     * @return array|string
88
     */
89
    public function get($extension = null)
90
    {
91
        $extractor = &gplcart_static(__METHOD__ . "$extension");
92
93
        if (isset($extractor)) {
94
            return $extractor;
95
        }
96
97
        $extractors = $this->getDefault();
98
99
        if (isset($extension)) {
100
            $extractor = empty($extractors[$extension]) ? '' : $extractors[$extension];
101
        } else {
102
            $extractor = $extractors;
103
        }
104
105
        $this->hook->attach('module.extractor.get', $extractor, $extension, $this);
106
        return $extractor;
107
    }
108
109
    /**
110
     * Returns an array of default extractors keyed by supported file extension
111
     * @return array
112
     */
113
    protected function getDefault()
114
    {
115
        return array(
116
            'json' => array($this, 'extractFromFileJson'),
117
            'js' => array(
118
                static::PATTERN_JS),
119
            'twig' => array(
120
                static::PATTERN_TWIG,
121
                static::PATTERN_JS
122
            ),
123
            'php' => array(
124
                static::PATTERN_PHP_METHOD,
125
                static::PATTERN_PHP_FUNCTION,
126
                static::PATTERN_JS,
127
                static::PATTERN_ANNOTATION_AFTER,
128
                static::PATTERN_ANNOTATION_BEFORE
129
            )
130
        );
131
    }
132
133
    /**
134
     * Extract strings from module.json
135
     * @param string $file
136
     * @return array
137
     */
138
    protected function extractFromFileJson($file)
139
    {
140
        $extracted = array();
141
        if (basename($file) === 'module.json') {
142
            $content = json_decode(file_get_contents($file), true);
143
            if (!empty($content['name'])) {
144
                $extracted[] = $content['name'];
145
            }
146
147
            if (!empty($content['description'])) {
148
                $extracted[] = $content['description'];
149
            }
150
        }
151
152
        return $extracted;
153
    }
154
155
    /**
156
     * Returns an array of extracted strings from a file
157
     * @param string $file
158
     * @return array
159
     */
160
    public function extractFromFile($file)
161
    {
162
        $extractor = $this->get(pathinfo($file, PATHINFO_EXTENSION));
163
164
        if (empty($extractor)) {
165
            return array();
166
        }
167
168
        if (is_callable($extractor)) {
169
            return call_user_func_array($extractor, array($file));
170
        }
171
172
        $handle = fopen($file, 'r');
173
174
        if (!is_resource($handle)) {
175
            return array();
176
        }
177
178
        $lines = self::MAX_LINES;
179
180
        $extracted = array();
181
        while ($lines && $line = fgets($handle, self::MAX_COLS)) {
182
            $extracted = array_merge($extracted, $this->extractFromString($line, $extractor));
183
            $lines--;
184
        }
185
186
        fclose($handle);
187
        return $extracted;
188
    }
189
190
    /**
191
     * Returns an array of extracted strings from a source string
192
     * @param string $string
193
     * @param string|array $regexp_patterns
194
     * @return array
195
     */
196
    public function extractFromString($string, $regexp_patterns)
197
    {
198
        $result = null;
199
        $this->hook->attach('module.extractor.extract', $string, $regexp_patterns, $result, $this);
200
201
        if (isset($result)) {
202
            return $result;
203
        }
204
205
        $extracted = array();
206
        foreach ((array) $regexp_patterns as $pattern) {
207
            $matches = array();
208
            preg_match_all($pattern, $string, $matches);
209
            if (!empty($matches[2])) {
210
                $extracted = array_merge($extracted, $this->clean($matches[2]));
211
            }
212
        }
213
214
        return $extracted;
215
    }
216
217
    /**
218
     * Clean up an array of extracted strings or a single string
219
     * @param array|string $items
220
     * @return array
221
     */
222
    protected function clean($items)
223
    {
224
        $cleaned = array();
225
        foreach ((array) $items as $item) {
226
            // Remove double/single quotes and whitespaces from the beginning and end of a string
227
            $str = trim(preg_replace('/^(\'(.*)\'|"(.*)")$/', '$2$3', $item));
228
            if ($str !== '') {
229
                $cleaned[] = $str;
230
            }
231
        }
232
233
        return $cleaned;
234
    }
235
236
    /**
237
     * Returns an array of scanned files to extract from or counts them
238
     * @param array $options
239
     * @return integer|array
240
     */
241
    public function scan(array $options)
242
    {
243
        $result = null;
244
        $this->hook->attach('module.extractor.scan', $options, $result, $this);
245
246
        if (isset($result)) {
247
            return $result;
248
        }
249
250
        $scanned = array();
251
        foreach ((array) $options['directory'] as $directory) {
252
            $scanned = array_merge($scanned, $this->scanRecursive($directory));
253
        }
254
255
        $files = array_filter($scanned, function ($file) {
256
            return $this->isSupportedFile($file);
257
        });
258
259
        if (!empty($options['count'])) {
260
            return count($files);
261
        }
262
263
        sort($files);
264
        return $files;
265
    }
266
267
    /**
268
     * Whether the file can be parsed
269
     * @param string $file
270
     * @return bool
271
     */
272
    public function isSupportedFile($file)
273
    {
274
        return is_file($file) && in_array(pathinfo($file, PATHINFO_EXTENSION), array_keys($this->get()));
275
    }
276
277
    /**
278
     * Returns an array of directories to be scanned
279
     * @return array
280
     */
281
    public function getScannedDirectories()
282
    {
283
        $directories = array(
284
            GC_DIR_CORE,
285
            GC_DIR_CONFIG,
286
            $this->module->getDirectory('frontend'),
287
            $this->module->getDirectory('backend')
288
        );
289
290
        $this->hook->attach('module.extractor.directories', $directories, $this);
291
        return $directories;
292
    }
293
294
    /**
295
     * Recursive scans files in a directory
296
     * @param string $directory
297
     * @param array $results
298
     * @return array
299
     */
300
    protected function scanRecursive($directory, &$results = array())
301
    {
302
        $directory = gplcart_path_normalize($directory);
303
304
        if (strpos($directory, '/override/') !== false) {
305
            return $results; // Exclude "override" directories
306
        }
307
308
        if (strpos($directory, '/vendor/') !== false) {
309
            return $results; // Exclude "vendor" directories
310
        }
311
312
        foreach (new DirectoryIterator($directory) as $file) {
313
            $realpath = $file->getRealPath();
314
            if ($file->isDir() && !$file->isDot()) {
315
                $this->scanRecursive($realpath, $results);
316
                $results[] = $realpath;
317
            } else if ($file->isFile()) {
318
                $results[] = $realpath;
319
            }
320
        }
321
322
        return $results;
323
    }
324
325
}
326