Passed
Pull Request — master (#50)
by Wilmer
02:02
created

AssetConverter::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
1
<?php
2
declare(strict_types = 1);
3
4
namespace Yiisoft\Asset;
5
6
use Psr\Log\LoggerInterface;
7
use Yiisoft\Aliases\Aliases;
8
9
/**
10
 * AssetConverter supports conversion of several popular script formats into JS or CSS scripts.
11
 *
12
 * It is used by [[AssetManager]] to convert files after they have been published.
13
 */
14
class AssetConverter implements AssetConverterInterface
15
{
16
    /**
17
     * Aliases component.
18
     *
19
     * @var Aliases $aliase
20
     */
21
    private $aliases;
22
23
    /**
24
     * @var array the commands that are used to perform the asset conversion.
25
     *            The keys are the asset file extension names, and the values are the corresponding
26
     *            target script types (either "css" or "js") and the commands used for the conversion.
27
     *
28
     * You may also use a [path alias](guide:concept-aliases) to specify the location of the command:
29
     *
30
     * ```php
31
     * [
32
     *     'styl' => ['css', '@app/node_modules/bin/stylus < {from} > {to}'],
33
     * ]
34
     * ```
35
     */
36
    public $commands = [
37
        'less'   => ['css', 'lessc {from} {to} --no-color --source-map'],
38
        'scss'   => ['css', 'sass {from} {to} --sourcemap'],
39
        'sass'   => ['css', 'sass {from} {to} --sourcemap'],
40
        'styl'   => ['css', 'stylus < {from} > {to}'],
41
        'coffee' => ['js', 'coffee -p {from} > {to}'],
42
        'ts'     => ['js', 'tsc --out {to} {from}'],
43
    ];
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
    /**
54
     * @var callable a PHP callback, which should be invoked to check whether asset conversion result is outdated.
55
     *               It will be invoked only if conversion target file exists and its modification time is older then the one of source file.
56
     *               Callback should match following signature:
57
     *
58
     * ```php
59
     * function (string $basePath, string $sourceFile, string $targetFile, string $sourceExtension, string $targetExtension) : bool
60
     * ```
61
     *
62
     * where $basePath is the asset source directory; $sourceFile is the asset source file path, relative to $basePath;
63
     * $targetFile is the asset target file path, relative to $basePath; $sourceExtension is the source asset file extension
64
     * and $targetExtension is the target asset file extension, respectively.
65
     *
66
     * It should return `true` is case asset should be reconverted.
67
     * For example:
68
     *
69
     * ```php
70
     * function ($basePath, $sourceFile, $targetFile, $sourceExtension, $targetExtension) {
71
     *     if (YII_ENV !== 'dev') {
72
     *         return false;
73
     *     }
74
     *
75
     *     $resultModificationTime = @filemtime("$basePath/$result");
76
     *     foreach (FileHelper::findFiles($basePath, ['only' => ["*.{$sourceExtension}"]]) as $filename) {
77
     *         if ($resultModificationTime < @filemtime($filename)) {
78
     *             return true;
79
     *         }
80
     *     }
81
     *
82
     *     return false;
83
     * }
84
     * ```
85
     */
86
    public $isOutdatedCallback;
87
88
    /**
89
     * @var LoggerInterface $logger
90
     */
91
    private $logger;
92
93
    /**
94
     * AssetConverter constructor.
95
     *
96
     * @param Aliases $aliases
97
     */
98 17
    public function __construct(Aliases $aliases, LoggerInterface $logger)
99
    {
100 17
        $this->aliases = $aliases;
101 17
        $this->logger = $logger;
102
    }
103
104
    /**
105
     * Converts a given asset file into a CSS or JS file.
106
     *
107
     * @param string $asset    the asset file path, relative to $basePath
108
     * @param string $basePath the directory the $asset is relative to.
109
     *
110
     * @return string the converted asset file path, relative to $basePath.
111
     */
112 17
    public function convert($asset, $basePath): string
113
    {
114 17
        $pos = strrpos($asset, '.');
115 17
        if ($pos !== false) {
116 17
            $srcExt = substr($asset, $pos + 1);
117 17
            if (isset($this->commands[$srcExt])) {
118 4
                [$ext, $command] = $this->commands[$srcExt];
119 4
                $result = substr($asset, 0, $pos + 1).$ext;
120 4
                if ($this->forceConvert || $this->isOutdated($basePath, $asset, $result, $srcExt, $ext)) {
121 4
                    $this->runCommand($command, $basePath, $asset, $result);
122
                }
123
124 4
                return $result;
125
            }
126
        }
127
128 13
        return $asset;
129
    }
130
131
    /**
132
     * Checks whether asset convert result is outdated, and thus should be reconverted.
133
     *
134
     * @param string $basePath        the directory the $asset is relative to.
135
     * @param string $sourceFile      the asset source file path, relative to [[$basePath]].
136
     * @param string $targetFile      the converted asset file path, relative to [[$basePath]].
137
     * @param string $sourceExtension source asset file extension.
138
     * @param string $targetExtension target asset file extension.
139
     *
140
     * @return bool whether asset is outdated or not.
141
     *
142
     * @since 3.0.0
143
     */
144 4
    protected function isOutdated($basePath, $sourceFile, $targetFile, $sourceExtension, $targetExtension)
145
    {
146 4
        $resultModificationTime = @filemtime("$basePath/$targetFile");
147 4
        if ($resultModificationTime === false || $resultModificationTime === null) {
148 4
            return true;
149
        }
150
151 3
        if ($resultModificationTime < @filemtime("$basePath/$sourceFile")) {
152 1
            return true;
153
        }
154
155 3
        if ($this->isOutdatedCallback === null) {
156 2
            return false;
157
        }
158
159 1
        return call_user_func($this->isOutdatedCallback, $basePath, $sourceFile, $targetFile, $sourceExtension, $targetExtension);
160
    }
161
162
    /**
163
     * Runs a command to convert asset files.
164
     *
165
     * @param string $command  the command to run. If prefixed with an `@` it will be treated as a [path alias](guide:concept-aliases).
166
     * @param string $basePath asset base path and command working directory
167
     * @param string $asset    the name of the asset file
168
     * @param string $result   the name of the file to be generated by the converter command
169
     *
170
     * @throws \Exception when the command fails and YII_DEBUG is true.
171
     *                                   In production mode the error will be logged.
172
     *
173
     * @return bool true on success, false on failure. Failures will be logged.
174
     */
175 4
    protected function runCommand($command, $basePath, $asset, $result)
176
    {
177 4
        $command = $this->aliases->get($command);
178
179 4
        $command = strtr($command, [
0 ignored issues
show
Bug introduced by
It seems like $command can also be of type boolean; however, parameter $str of strtr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

179
        $command = strtr(/** @scrutinizer ignore-type */ $command, [
Loading history...
180 4
            '{from}' => escapeshellarg("$basePath/$asset"),
181 4
            '{to}'   => escapeshellarg("$basePath/$result"),
182
        ]);
183
        $descriptor = [
184 4
            1 => ['pipe', 'w'],
185
            2 => ['pipe', 'w'],
186
        ];
187 4
        $pipes = [];
188 4
        $proc = proc_open($command, $descriptor, $pipes, $basePath);
189 4
        $stdout = stream_get_contents($pipes[1]);
190 4
        $stderr = stream_get_contents($pipes[2]);
191
192 4
        foreach ($pipes as $pipe) {
193 4
            fclose($pipe);
194
        }
195
196 4
        $status = proc_close($proc);
0 ignored issues
show
Bug introduced by
It seems like $proc can also be of type false; however, parameter $process of proc_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

196
        $status = proc_close(/** @scrutinizer ignore-type */ $proc);
Loading history...
197
198 4
        if ($status === 0) {
199 4
            $this->logger->debug("Converted $asset into $result:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr", [__METHOD__]);
200
        } elseif (YII_DEBUG) {
0 ignored issues
show
Bug introduced by
The constant Yiisoft\Asset\YII_DEBUG was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
201
            throw new \Exception("AssetConverter command '$command' failed with exit code $status:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr");
202
        } else {
203
            $this->logger->error("AssetConverter command '$command' failed with exit code $status:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr", [__METHOD__]);
204
        }
205
206 4
        return $status === 0;
207
    }
208
}
209