Passed
Pull Request — master (#19505)
by Fedonyuk
10:48
created

AssetConverter::runCommand()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 38
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 5.2259

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 26
c 1
b 0
f 0
nc 6
nop 4
dl 0
loc 38
ccs 19
cts 24
cp 0.7917
crap 5.2259
rs 9.1928
1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://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 formats into JS or CSS files.
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
     * The command placeholders: `{from}` - source file, `{to}` - converted file.
30
     *
31
     * You may also use a [path alias](guide:concept-aliases) to specify the location of the command:
32
     *
33
     * ```php
34
     * [
35
     *     'styl' => ['css', '@app/node_modules/bin/stylus < {from} > {to}'],
36
     * ]
37
     * ```
38
     *
39
     * @see https://sass-lang.com/documentation/cli SASS/SCSS
40
     */
41
    public $commands = [
42
        'less' => ['css', 'lessc {from} {to} --no-color --source-map'],
43
        'scss' => ['css', 'sass --style=compressed {from} {to}'],
44
        'sass' => ['css', 'sass --style=compressed {from} {to}'],
45
        'styl' => ['css', 'stylus < {from} > {to}'],
46
        'coffee' => ['js', 'coffee -p {from} > {to}'],
47
        'ts' => ['js', 'tsc --out {to} {from}'],
48
    ];
49
    /**
50
     * @var bool whether the source asset file should be converted even if its result already exists.
51
     * You may want to set this to be `true` during the development stage to make sure the converted
52
     * assets are always up-to-date. Do not set this to true on production servers as it will
53
     * significantly degrade the performance.
54
     */
55
    public $forceConvert = false;
56
57
58
    /**
59
     * Converts a given asset file into a CSS or JS file.
60
     * @param string $asset the asset file path, relative to $basePath
61
     * @param string $basePath the directory the $asset is relative to.
62
     * @return string the converted asset file path, relative to $basePath.
63
     */
64 34
    public function convert($asset, $basePath)
65
    {
66 34
        $pos = strrpos($asset, '.');
67 34
        if ($pos !== false) {
68 34
            $ext = substr($asset, $pos + 1);
69 34
            if (isset($this->commands[$ext])) {
70 2
                list($ext, $command) = $this->commands[$ext];
71 2
                $result = substr($asset, 0, $pos + 1) . $ext;
72 2
                if ($this->forceConvert || @filemtime("$basePath/$result") < @filemtime("$basePath/$asset")) {
73 2
                    $this->runCommand($command, $basePath, $asset, $result);
74
                }
75
76 2
                return $result;
77
            }
78
        }
79
80 32
        return $asset;
81
    }
82
83
    /**
84
     * Runs a command to convert asset files.
85
     * @param string $command the command to run. If prefixed with an `@` it will be treated as a [path alias](guide:concept-aliases).
86
     * @param string $basePath asset base path and command working directory
87
     * @param string $asset the name of the asset file
88
     * @param string $result the name of the file to be generated by the converter command
89
     * @return bool true on success, false on failure. Failures will be logged.
90
     * @throws \yii\base\Exception when the command fails and YII_DEBUG is true.
91
     * In production mode the error will be logged.
92
     */
93 2
    protected function runCommand($command, $basePath, $asset, $result)
94
    {
95 2
        $command = strtr($command, [
96 2
            '{from}' => escapeshellarg("$basePath/$asset"),
97 2
            '{to}' => escapeshellarg("$basePath/$result"),
98
        ]);
99 2
        $command = preg_replace_callback(
100 2
            '/@[^@]+/',
101 2
            static function ($matches) {
102
                $to = Yii::getAlias($matches[0], false);
103
104
                return $to === false ? $matches[0] : escapeshellarg($to);
105 2
            },
106
            $command
107
        );
108
        
109
        $descriptor = [
110 2
            1 => ['pipe', 'w'],
111
            2 => ['pipe', 'w'],
112
        ];
113 2
        $pipes = [];
114 2
        $proc = proc_open($command, $descriptor, $pipes, $basePath);
115 2
        $stdout = stream_get_contents($pipes[1]);
116 2
        $stderr = stream_get_contents($pipes[2]);
117 2
        foreach ($pipes as $pipe) {
118 2
            fclose($pipe);
119
        }
120 2
        $status = proc_close($proc);
121
122 2
        if ($status === 0) {
123 2
            Yii::debug("Converted $asset into $result:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr", __METHOD__);
124
        } elseif (YII_DEBUG) {
125
            throw new Exception("AssetConverter command '$command' failed with exit code $status:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr");
126
        } else {
127
            Yii::error("AssetConverter command '$command' failed with exit code $status:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr", __METHOD__);
128
        }
129
130 2
        return $status === 0;
131
    }
132
}
133