Completed
Push — 2.1 ( 25ffc7...fdd761 )
by
unknown
12:30
created

AssetConverter::isOutdated()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 4
nop 5
crap 5
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\web;
9
10
use Yii;
11
use yii\base\Component;
12
use yii\base\Exception;
13
14
/**
15
 * AssetConverter supports conversion of several popular script formats into JS or CSS scripts.
16
 *
17
 * It is used by [[AssetManager]] to convert files after they have been published.
18
 *
19
 * @author Qiang Xue <[email protected]>
20
 * @since 2.0
21
 */
22
class AssetConverter extends Component implements AssetConverterInterface
23
{
24
    /**
25
     * @var array the commands that are used to perform the asset conversion.
26
     * The keys are the asset file extension names, and the values are the corresponding
27
     * target script types (either "css" or "js") and the commands used for the conversion.
28
     *
29
     * You may also use a [path alias](guide:concept-aliases) to specify the location of the command:
30
     *
31
     * ```php
32
     * [
33
     *     'styl' => ['css', '@app/node_modules/bin/stylus < {from} > {to}'],
34
     * ]
35
     * ```
36
     */
37
    public $commands = [
38
        'less' => ['css', 'lessc {from} {to} --no-color --source-map'],
39
        'scss' => ['css', 'sass {from} {to} --sourcemap'],
40
        'sass' => ['css', 'sass {from} {to} --sourcemap'],
41
        'styl' => ['css', 'stylus < {from} > {to}'],
42
        'coffee' => ['js', 'coffee -p {from} > {to}'],
43
        'ts' => ['js', 'tsc --out {to} {from}'],
44
    ];
45
    /**
46
     * @var bool whether the source asset file should be converted even if its result already exists.
47
     * You may want to set this to be `true` during the development stage to make sure the converted
48
     * assets are always up-to-date. Do not set this to true on production servers as it will
49
     * significantly degrade the performance.
50
     */
51
    public $forceConvert = false;
52
    /**
53
     * @var callable a PHP callback, which should be invoked to check whether asset conversion result is outdated.
54
     * It will be invoked only if conversion target file exists and its modification time is older then the one of source file.
55
     * Callback should match following signature:
56
     *
57
     * ```php
58
     * function (string $basePath, string $sourceFile, string $targetFile, string $sourceExtension, string $targetExtension) : bool
59
     * ```
60
     *
61
     * where $basePath is the asset source directory; $sourceFile is the asset source file path, relative to $basePath;
62
     * $targetFile is the asset target file path, relative to $basePath; $sourceExtension is the source asset file extension
63
     * and $targetExtension is the target asset file extension, respectively.
64
     *
65
     * It should return `true` is case asset should be reconverted.
66
     * For example:
67
     *
68
     * ```php
69
     * function ($basePath, $sourceFile, $targetFile, $sourceExtension, $targetExtension) {
70
     *     if (YII_ENV !== 'dev') {
71
     *         return false;
72
     *     }
73
     *
74
     *     $resultModificationTime = @filemtime("$basePath/$result");
75
     *     foreach (FileHelper::findFiles($basePath, ['only' => ["*.{$sourceExtension}"]]) as $filename) {
76
     *         if ($resultModificationTime < @filemtime($filename)) {
77
     *             return true;
78
     *         }
79
     *     }
80
     *
81
     *     return false;
82
     * }
83
     * ```
84
     *
85
     * @since 2.1.0
86
     */
87
    public $isOutdatedCallback;
88
89
90
    /**
91
     * Converts a given asset file into a CSS or JS file.
92
     * @param string $asset the asset file path, relative to $basePath
93
     * @param string $basePath the directory the $asset is relative to.
94
     * @return string the converted asset file path, relative to $basePath.
95
     */
96 30
    public function convert($asset, $basePath)
97
    {
98 30
        $pos = strrpos($asset, '.');
99 30
        if ($pos !== false) {
100 30
            $srcExt = substr($asset, $pos + 1);
101 30
            if (isset($this->commands[$srcExt])) {
102 4
                [$ext, $command] = $this->commands[$srcExt];
0 ignored issues
show
Bug introduced by
The variable $ext does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $command does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
103 4
                $result = substr($asset, 0, $pos + 1) . $ext;
104 4
                if ($this->forceConvert || $this->isOutdated($basePath, $asset, $result, $srcExt, $ext)) {
105 4
                    $this->runCommand($command, $basePath, $asset, $result);
106
                }
107
108 4
                return $result;
109
            }
110
        }
111
112 26
        return $asset;
113
    }
114
115
    /**
116
     * Checks whether asset convert result is outdated, and thus should be reconverted.
117
     * @param string $basePath the directory the $asset is relative to.
118
     * @param string $sourceFile the asset source file path, relative to [[$basePath]].
119
     * @param string $targetFile the converted asset file path, relative to [[$basePath]].
120
     * @param string $sourceExtension source asset file extension.
121
     * @param string $targetExtension target asset file extension.
122
     * @return bool whether asset is outdated or not.
123
     * @since 2.1.0
124
     */
125 4
    protected function isOutdated($basePath, $sourceFile, $targetFile, $sourceExtension, $targetExtension)
126
    {
127 4
        $resultModificationTime = @filemtime("$basePath/$targetFile");
128 4
        if ($resultModificationTime === false || $resultModificationTime === null) {
129 4
            return true;
130
        }
131
132 3
        if ($resultModificationTime < @filemtime("$basePath/$sourceFile")) {
133 1
            return true;
134
        }
135
136 3
        if ($this->isOutdatedCallback === null) {
137 2
            return false;
138
        }
139
140 1
        return call_user_func($this->isOutdatedCallback, $basePath, $sourceFile, $targetFile, $sourceExtension, $targetExtension);
141
    }
142
143
    /**
144
     * Runs a command to convert asset files.
145
     * @param string $command the command to run. If prefixed with an `@` it will be treated as a [path alias](guide:concept-aliases).
146
     * @param string $basePath asset base path and command working directory
147
     * @param string $asset the name of the asset file
148
     * @param string $result the name of the file to be generated by the converter command
149
     * @return bool true on success, false on failure. Failures will be logged.
150
     * @throws \yii\base\Exception when the command fails and YII_DEBUG is true.
151
     * In production mode the error will be logged.
152
     */
153 4
    protected function runCommand($command, $basePath, $asset, $result)
154
    {
155 4
        $command = Yii::getAlias($command);
156
157 4
        $command = strtr($command, [
158 4
            '{from}' => escapeshellarg("$basePath/$asset"),
159 4
            '{to}' => escapeshellarg("$basePath/$result"),
160
        ]);
161
        $descriptor = [
162 4
            1 => ['pipe', 'w'],
163
            2 => ['pipe', 'w'],
164
        ];
165 4
        $pipes = [];
166 4
        $proc = proc_open($command, $descriptor, $pipes, $basePath);
167 4
        $stdout = stream_get_contents($pipes[1]);
168 4
        $stderr = stream_get_contents($pipes[2]);
169 4
        foreach ($pipes as $pipe) {
170 4
            fclose($pipe);
171
        }
172 4
        $status = proc_close($proc);
173
174 4
        if ($status === 0) {
175 4
            Yii::debug("Converted $asset into $result:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr", __METHOD__);
176
        } elseif (YII_DEBUG) {
177
            throw new Exception("AssetConverter command '$command' failed with exit code $status:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr");
178
        } else {
179
            Yii::error("AssetConverter command '$command' failed with exit code $status:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr", __METHOD__);
180
        }
181
182 4
        return $status === 0;
183
    }
184
}
185