Importer::getImportedFiles()   A
last analyzed

Complexity

Conditions 1
Paths 1

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 1
eloc 2
nc 1
nop 0
1
<?php
2
/*
3
 * This file is part of the ILess
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace ILess;
10
11
use ILess\Cache\CacheInterface;
12
use ILess\Exception\Exception;
13
use ILess\Exception\ImportException;
14
use ILess\Exception\ParserException;
15
use ILess\Importer\ImporterInterface;
16
use ILess\Node\RulesetNode;
17
use ILess\Parser\Core;
18
19
/**
20
 * Import.
21
 */
22
class Importer
23
{
24
    /**
25
     * The context.
26
     *
27
     * @var Context
28
     */
29
    protected $context;
30
31
    /**
32
     * The cache.
33
     *
34
     * @var CacheInterface
35
     */
36
    protected $cache;
37
38
    /**
39
     * Array of importers.
40
     *
41
     * @var array
42
     */
43
    protected $importers = [];
44
45
    /**
46
     * Array of imported files.
47
     *
48
     * @var array
49
     */
50
    protected $importedFiles = [];
51
52
    /**
53
     * @var PluginManager
54
     */
55
    protected $pluginManager;
56
57
    /**
58
     * Constructor.
59
     *
60
     * @param Context $context The context
61
     * @param array $importers Array of importers
62
     * @param CacheInterface $cache The cache
63
     * @param PluginManager $manager
64
     */
65
    public function __construct(Context $context, array $importers, CacheInterface $cache, PluginManager $manager = null)
66
    {
67
        $this->context = $context;
68
        $this->pluginManager = $manager;
69
        $this->registerImporters($importers);
70
        $this->cache = $cache;
71
    }
72
73
    /**
74
     * Sets The context.
75
     *
76
     * @param Context $context
77
     *
78
     * @return Importer
79
     */
80
    public function setEnvironment(Context $context)
81
    {
82
        $this->context = $context;
83
84
        return $this;
85
    }
86
87
    /**
88
     * Sets the cache.
89
     *
90
     * @param CacheInterface $cache
91
     *
92
     * @return Importer
93
     */
94
    public function setCache(CacheInterface $cache)
95
    {
96
        $this->cache = $cache;
97
98
        return $this;
99
    }
100
101
    /**
102
     * Returns the cache.
103
     *
104
     * @return CacheInterface
105
     */
106
    public function getCache()
107
    {
108
        return $this->cache;
109
    }
110
111
    /**
112
     * Returns the context.
113
     *
114
     * @return Context
115
     */
116
    public function getContext()
117
    {
118
        return $this->context;
119
    }
120
121
    /**
122
     * Imports the file.
123
     *
124
     * @param string $path The path to import. Path will be searched by the importers
125
     * @param bool $tryAppendLessExtension Whether to try appending the less extension (if the path has no extension)
126
     * @param array $importOptions Import options
127
     * @param int $index Current index
128
     *
129
     * @return array
130
     *
131
     * @throws ImportException If the $path could not be imported
132
     */
133
    public function import(
134
        $path,
135
        $tryAppendLessExtension = false,
136
        FileInfo $currentFileInfo,
137
        array $importOptions = [],
138
        $index = 0
139
    ) {
140
        $plugin = isset($importOptions['plugin']) && $importOptions['plugin'];
141
        $inline = isset($importOptions['inline']) && $importOptions['inline'];
142
        $css = isset($importOptions['css']) && $importOptions['css'];
143
144
        if ($tryAppendLessExtension && !pathinfo($path, PATHINFO_EXTENSION)) {
145
            if ($plugin) {
146
                $path .= '.php';
147
            } else {
148
                $path .= '.less';
149
            }
150
        }
151
152
        // we must generate separate cache for inline and css files to avoid problems
153
        // when someone wants to import the less file first as inline import and than normally
154
        $cacheKey = $this->generateCacheKey(
155
            (Util::isPathAbsolute($path) ? $path : $currentFileInfo->currentDirectory . $path)
156
            . 'css-' . (int) $inline . 'inline-' . (int) $css
157
        );
158
159
        // do we have a file in the cache?
160
        if ($this->cache->has($cacheKey)) {
161
            // check the modified timestamp
162
            $file = $this->cache->get($cacheKey);
163
            // search
164
            foreach ($this->importers as $importer) {
165
                /* @var $file ImportedFile */
166
                /* @var $importer ImporterInterface */
167
                $lastModified = $importer->getLastModified($path, $currentFileInfo);
168
                if ($lastModified !== false && $lastModified === $file->getLastModified()) {
169
                    // the modification time is the same, take the one from cache
170
                    return $this->doImport($file, $path, $currentFileInfo, $importOptions, true);
171
                }
172
            }
173
        }
174
175
        foreach ($this->importers as $importer) {
176
            /* @var $importer ImporterInterface */
177
            $file = $importer->import($path, $currentFileInfo);
178
179
            // import is handled by the importer
180
            if ($file instanceof ImportedFile) {
181
                if ($plugin) {
182
183
                    // create dummy ruleset which will hold the functions
184
                    $ruleset = new RulesetNode([], []);
185
                    $ruleset->root = false;
186
                    $ruleset->functions[] = $file->getPath();
187
188
                    $file->setRuleset($ruleset);
189
190
                    return [
191
                        true,
192
                        $file,
193
                    ];
194
                } else {
195
                    $result = $this->doImport($file, $path, $currentFileInfo, $importOptions);
196
                    /* @var $file ImportedFile */
197
                    list(, $file) = $result;
198
                    // save the cache
199
                    $this->cache->set($cacheKey, $file);
200
201
                    return $result;
202
                }
203
            }
204
        }
205
206
        throw new ImportException(sprintf("'%s' wasn't found.", $path), $index, $currentFileInfo);
207
    }
208
209
    /**
210
     * Does the import.
211
     *
212
     * @param ImportedFile $file The imported file
213
     * @param string $path The original path
214
     * @param FileInfo $currentFileInfo Current file info
215
     * @param array $importOptions Import options
216
     * @param bool $fromCache Is the imported file coming from cache?
217
     *
218
     * @throws ParserException
219
     * @throws Exception
220
     *
221
     * @return array
222
     */
223
    protected function doImport(
224
        ImportedFile $file,
225
        $path,
226
        FileInfo $currentFileInfo,
227
        array $importOptions = [],
228
        $fromCache = false
229
    ) {
230
        $newEnv = Context::createCopyForCompilation($this->context, $this->context->frames);
231
232
        $newFileInfo = clone $currentFileInfo;
233
234
        if ($this->context->relativeUrls) {
235
            // Pass on an updated rootPath if path of imported file is relative and file
236
            // is in a (sub|sup) directory
237
            //
238
            // Examples:
239
            // - If path of imported file is 'module/nav/nav.less' and rootPath is 'less/',
240
            //   then rootPath should become 'less/module/nav/'
241
            // - If path of imported file is '../mixins.less' and rootPath is 'less/',
242
            //   then rootPath should become 'less/../'
243
            if (!Util::isPathAbsolute($path) && (($lastSlash = strrpos($path, '/')) !== false)) {
244
                $relativeSubDirectory = substr($path, 0, $lastSlash + 1);
245
                $newFileInfo->rootPath = $newFileInfo->rootPath . $relativeSubDirectory;
246
            }
247
        }
248
249
        // we need to clone here, to prevent modification of node current info object
250
        $newEnv->currentFileInfo = $newFileInfo;
251
        $newEnv->processImports = false;
252
253
        if ($currentFileInfo->reference
254
            || (isset($importOptions['reference']) && $importOptions['reference'])
255
        ) {
256
            $newEnv->currentFileInfo->reference = true;
257
        }
258
259
        $key = $file->getPath();
260
        $root = null;
261
        $alreadyImported = false;
262
263
        // check for already imported file
264
        if (isset($this->importedFiles[$key])) {
265
            $alreadyImported = true;
266
        } elseif (!$file->getRuleset()) {
267
            try {
268
                // we do not parse the root but load the file as is
269
                if (isset($importOptions['inline']) && $importOptions['inline']) {
270
                    $root = $file->getContent();
271
                } else {
272
                    $parser = new Core($newEnv, $this, $this->pluginManager);
273
                    $root = $parser->parseFile($file, true);
274
                    $root->root = false;
275
                    $root->firstRoot = false;
276
                }
277
                $file->setRuleset($root);
0 ignored issues
show
Bug introduced by
It seems like $root defined by $parser->parseFile($file, true) on line 273 can also be of type object<ILess\Parser\Core>; however, ILess\ImportedFile::setRuleset() does only seem to accept string|object<ILess\Node\RulesetNode>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
278
                // we need to catch parse exceptions
279
            } catch (Exception $e) {
280
                // rethrow
281
                throw $e;
282
            } catch (\Exception $error) {
283
                $file->setError($error);
284
            }
285
286
            $this->setImportedFile($key, $file, $path, $currentFileInfo);
287
        } else {
288
            $this->setImportedFile($key, $file, $path, $currentFileInfo);
289
        }
290
291
        if ($fromCache) {
292
            $ruleset = $this->importedFiles[$key][0]->getRuleset();
293
            if ($ruleset instanceof Node) {
294
                // this is a workaround for reference and import one issues when taken cache
295
                $this->updateReferenceInCurrentFileInfo($ruleset, $newEnv->currentFileInfo);
296
            }
297
        }
298
299
        return [
300
            $alreadyImported,
301
            $this->importedFiles[$key][0],
302
        ];
303
    }
304
305
    /**
306
     * Updates the currentFileInfo object to the $value.
307
     *
308
     * @param Node $node The node to update
309
     * @param FileInfo $newInfo The new file info
310
     */
311
    protected function updateReferenceInCurrentFileInfo(Node $node, FileInfo $newInfo)
312
    {
313
        if (isset($node->currentFileInfo)) {
314
            $node->currentFileInfo->reference = $newInfo->reference;
315
            $node->currentFileInfo->rootPath = $newInfo->rootPath;
316
        }
317
318
        if (Node::propertyExists($node, 'rules') &&
319
            is_array($node->rules) && $node->rules
320
        ) {
321
            foreach ($node->rules as $rule) {
322
                $this->updateReferenceInCurrentFileInfo($rule, $newInfo);
323
            }
324
        }
325
    }
326
327
    /**
328
     * Returns the last modification time of the file.
329
     *
330
     * @param string $path
331
     * @param FileInfo $currentFileInfo
332
     *
333
     * @return int
334
     *
335
     * @throws Exception If there was an error
336
     */
337
    public function getLastModified($path, FileInfo $currentFileInfo)
338
    {
339
        foreach ($this->importers as $importer) {
340
            /* @var $importer ImporterInterface */
341
            $result = $importer->getLastModified($path, $currentFileInfo);
342
            if ($result !== null) {
343
                return $result;
344
            }
345
        }
346
347
        throw new ImportException(sprintf('Error getting last modification time of the file "%s".', $path));
348
    }
349
350
    /**
351
     * Registers an importer.
352
     *
353
     * @param ImporterInterface $importer
354
     * @param string $name The importer name (only for developer reference)
355
     * @param bool $prepend Prepend before current importers?
356
     *
357
     * @return Importer
358
     */
359
    public function registerImporter(ImporterInterface $importer, $name = null, $prepend = false)
360
    {
361
        // FIXME: what about more than one importer with the same class?
362
        $name = !is_null($name) ? $name : get_class($importer);
363
364
        if ($prepend) {
365
            // array unshift with preservation of keys
366
            $importers = array_reverse($this->importers, true);
367
            $importers[$name] = $importer;
368
            $this->importers = array_reverse($importers, true);
369
        } else {
370
            $this->importers[$name] = $importer;
371
        }
372
373
        return $this;
374
    }
375
376
    /**
377
     * Returns the importer with given name.
378
     *
379
     * @param string $name
380
     *
381
     * @return ImporterInterface
382
     */
383
    public function getImporter($name)
384
    {
385
        return isset($this->importers[$name]) ? $this->importers[$name] : null;
386
    }
387
388
    /**
389
     * Returns registered importers.
390
     *
391
     * @return array
392
     */
393
    public function getImporters()
394
    {
395
        return $this->importers;
396
    }
397
398
    /**
399
     * Registers an array of importers.
400
     *
401
     * @param array $importers
402
     *
403
     * @return Importer
404
     */
405
    public function registerImporters(array $importers)
406
    {
407
        foreach ($importers as $name => $importer) {
408
            $this->registerImporter($importer, is_numeric($name) ? null : $name);
409
        }
410
411
        return $this;
412
    }
413
414
    /**
415
     * Clears all importers.
416
     *
417
     * @return Importer
418
     */
419
    public function clearImporters()
420
    {
421
        $this->importers = [];
422
423
        return $this;
424
    }
425
426
    /**
427
     * Returns a list of imported files.
428
     *
429
     * @return array
430
     */
431
    public function getImportedFiles()
432
    {
433
        return $this->importedFiles;
434
    }
435
436
    /**
437
     * Sets the imported file.
438
     *
439
     * @param string $pathAbsolute The absolute path
440
     * @param ImportedFile $file The imported file
441
     * @param string $path The original path to import
442
     * @param FileInfo $currentFileInfo
443
     *
444
     * @return Importer
445
     */
446
    public function setImportedFile($pathAbsolute, ImportedFile $file, $path, FileInfo $currentFileInfo)
447
    {
448
        $this->importedFiles[$pathAbsolute] = [$file, $path, $currentFileInfo];
449
        // save for source map generation
450
        $this->context->setFileContent($pathAbsolute, $file->getContent());
451
452
        return $this;
453
    }
454
455
    /**
456
     * Returns the imported file.
457
     *
458
     * @param string $absolutePath The absolute path of the file
459
     * @param mixed $default The default when no file with given $path is already imported
460
     *
461
     * @return array Array(ImportedFile, $originalPath, CurrentFileInfo)
462
     */
463
    public function getImportedFile($absolutePath, $default = null)
464
    {
465
        return isset($this->importedFiles[$absolutePath]) ? $this->importedFiles[$absolutePath] : $default;
466
    }
467
468
    /**
469
     * Generates unique cache key for given $filename.
470
     *
471
     * @param string $filename
472
     *
473
     * @return string
474
     */
475
    protected function generateCacheKey($filename)
476
    {
477
        return Util::generateCacheKey($filename);
478
    }
479
}
480