1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Webrouse\AssetMacro; |
4
|
|
|
|
5
|
|
|
use Latte; |
6
|
|
|
use Latte\Macros\MacroSet; |
7
|
|
|
use Nette\Utils\Json; |
8
|
|
|
use Nette\Utils\Strings; |
9
|
|
|
use Webrouse\AssetMacro\Exceptions\AssetVersionNotFound; |
10
|
|
|
use Webrouse\AssetMacro\Exceptions\DirNotFoundException; |
11
|
|
|
use Webrouse\AssetMacro\Exceptions\FileNotFoundException; |
12
|
|
|
use Webrouse\AssetMacro\Exceptions\InvalidArgumentException; |
13
|
|
|
use Webrouse\AssetMacro\Exceptions\InvalidVariableException; |
14
|
|
|
|
15
|
|
|
|
16
|
1 |
|
class AssetMacro extends MacroSet |
17
|
|
|
{ |
18
|
|
|
|
19
|
|
|
const VERSIONS_AUTODETECT = NULL; |
20
|
|
|
|
21
|
|
|
const CONFIG_PROVIDER = 'assetMacroConfig'; |
22
|
|
|
|
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @param Latte\Compiler $compiler |
26
|
|
|
*/ |
27
|
1 |
|
public static function install(Latte\Compiler $compiler) |
28
|
|
|
{ |
29
|
1 |
|
$me = new self($compiler); |
30
|
1 |
|
$me->addMacro('asset', [$me, 'macroAsset']); |
31
|
1 |
|
} |
32
|
|
|
|
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @param Latte\MacroNode $node |
36
|
|
|
* @param Latte\PhpWriter $writer |
37
|
|
|
* @return string |
38
|
|
|
* @throws Latte\CompileException |
39
|
|
|
*/ |
40
|
1 |
|
public function macroAsset(Latte\MacroNode $node, Latte\PhpWriter $writer) |
41
|
|
|
{ |
42
|
1 |
|
$args = trim($node->args); |
43
|
|
|
|
44
|
|
|
// Validate arguments count |
45
|
1 |
|
$argsCount = $args === '' ? 0 : (substr_count($args, ',') + 1); |
46
|
1 |
|
if ($argsCount === 0) { |
47
|
1 |
|
throw new Latte\CompileException("Asset macro requires at least one argument."); |
48
|
|
|
} |
49
|
1 |
|
if ($argsCount > 3) { |
50
|
1 |
|
throw new Latte\CompileException("Asset macro must have no more than 3 arguments."); |
51
|
|
|
} |
52
|
|
|
|
53
|
1 |
|
return $writer->write( |
54
|
|
|
'echo %escape(' . self::class . '::generateAssetPath(' . |
55
|
1 |
|
'%node.word, %node.array, $basePath, $template->global->' . self::CONFIG_PROVIDER . '))'); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @param string $path |
61
|
|
|
* @param array $args |
62
|
|
|
* @param string $basePath |
63
|
|
|
* @param array $config |
64
|
|
|
* @return string |
65
|
|
|
*/ |
66
|
1 |
|
public static function generateAssetPath($path, array $args, $basePath, $config) |
67
|
|
|
{ |
68
|
1 |
|
list($relativePath, $format, $needed) = self::processArguments($path, $args); |
69
|
1 |
|
list($relativePath, $absolutePath, $wwwDir) = self::processPaths($relativePath, $needed, $config); |
70
|
1 |
|
if ($absolutePath === FALSE) { |
71
|
1 |
|
return ''; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
// Get asset version |
75
|
1 |
|
$versions = $config['versions'] === self::VERSIONS_AUTODETECT ? |
76
|
1 |
|
self::autodetectVersions($absolutePath, $wwwDir, $config['autodetectPaths']) : |
77
|
1 |
|
$config['versions']; |
78
|
1 |
|
$version = self::getAssetVersion($versions, $absolutePath, $config); |
79
|
|
|
|
80
|
1 |
|
return self::formatOutput($format, $basePath, $relativePath, $version); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Generate output according given format |
86
|
|
|
* @param string $format |
87
|
|
|
* @param string $basePath |
88
|
|
|
* @param string $relativePath |
89
|
|
|
* @param string $version |
90
|
|
|
* @return string |
91
|
|
|
*/ |
92
|
|
|
private static function formatOutput($format, $basePath, $relativePath, $version) |
93
|
|
|
{ |
94
|
1 |
|
return Strings::replace($format, |
95
|
1 |
|
'/%([^%]+)%/', |
96
|
1 |
|
function ($matches) use ($basePath, $format, $relativePath, $version) { |
97
|
1 |
|
switch ($matches[1]) { |
98
|
1 |
|
case 'url': |
99
|
1 |
|
return sprintf("%s/%s?v=%s", $basePath, $relativePath, $version); |
100
|
1 |
|
case 'version': |
101
|
1 |
|
return $version; |
102
|
1 |
|
case 'basePath': |
103
|
1 |
|
return $basePath; |
104
|
1 |
|
case 'dir': |
105
|
1 |
|
return dirname($relativePath); |
106
|
1 |
|
case 'file': |
107
|
1 |
|
return basename($relativePath); |
108
|
|
|
default: |
109
|
1 |
|
$msg = sprintf( |
110
|
|
|
"Asset macro: Invalid variable '%s' in format '%s'. " . |
111
|
1 |
|
"Use one of allowed variables: %%url%%, %%version%%, %%basePath%%, %%dir%%, %%file%%.", |
112
|
1 |
|
$matches[1], |
113
|
1 |
|
$format |
114
|
|
|
); |
115
|
1 |
|
throw new InvalidVariableException($msg); |
116
|
|
|
} |
117
|
1 |
|
}); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* @param string $path |
123
|
|
|
* @param array $args |
124
|
|
|
* @return array |
125
|
|
|
*/ |
126
|
1 |
|
private static function processArguments($path, array $args) |
127
|
|
|
{ |
128
|
1 |
|
$relativePath = $path; |
129
|
|
|
|
130
|
|
|
// Format argument |
131
|
1 |
View Code Duplication |
if (isset($args['format'])) { |
|
|
|
|
132
|
1 |
|
$format = $args['format']; |
133
|
|
|
|
134
|
1 |
|
} elseif (isset($args[0])) { |
135
|
1 |
|
$format = $args[0]; |
136
|
|
|
|
137
|
|
|
} else { |
138
|
1 |
|
$format = '%url%'; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
// Needed argument |
142
|
1 |
View Code Duplication |
if (isset($args['needed'])) { |
|
|
|
|
143
|
1 |
|
$needed = $args['needed']; |
144
|
|
|
|
145
|
1 |
|
} elseif (isset($args[1])) { |
146
|
1 |
|
$needed = $args[1]; |
147
|
|
|
|
148
|
|
|
} else { |
149
|
1 |
|
$needed = TRUE; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
// Validate arguments |
153
|
1 |
|
if ( ! is_string($relativePath)) { |
154
|
1 |
|
throw new InvalidArgumentException(sprintf( |
155
|
1 |
|
"Asset macro: Invalid type of the path argument. Required string. Given %s.", |
156
|
1 |
|
gettype($relativePath) |
157
|
|
|
)); |
158
|
|
|
} |
159
|
1 |
|
if ( ! is_string($format)) { |
160
|
1 |
|
throw new InvalidArgumentException(sprintf( |
161
|
1 |
|
"Asset macro: Invalid type of the format argument. Required string. Given %s.", |
162
|
1 |
|
gettype($format) |
163
|
|
|
)); |
164
|
|
|
} |
165
|
1 |
|
if ( ! is_bool($needed)) { |
166
|
1 |
|
throw new InvalidArgumentException(sprintf( |
167
|
1 |
|
"Asset macro: Invalid type of the needed argument. Required boolean. Given %s.", |
168
|
1 |
|
gettype($needed) |
169
|
|
|
)); |
170
|
|
|
} |
171
|
|
|
|
172
|
1 |
|
return [$relativePath, $format, $needed]; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* @param $relativePath |
178
|
|
|
* @param $needed |
179
|
|
|
* @param array $config |
180
|
|
|
* @return array |
181
|
|
|
*/ |
182
|
1 |
|
private static function processPaths($relativePath, $needed, array $config) |
183
|
|
|
{ |
184
|
1 |
|
$relativePath = ltrim($relativePath, '\\/'); |
185
|
1 |
|
if (($wwwDir = realpath($config['wwwDir'])) === FALSE) { |
186
|
1 |
|
throw new DirNotFoundException(sprintf("Www dir '%s' not found.", $config['wwwDir'])); |
187
|
|
|
} |
188
|
|
|
|
189
|
1 |
|
if (($absolutePath = realpath($wwwDir . DIRECTORY_SEPARATOR . $relativePath)) === FALSE) { |
190
|
1 |
|
if ($needed) { |
191
|
1 |
|
$msg = sprintf("Asset '%s' not found.", $relativePath); |
192
|
1 |
|
if ($config['missingAsset'] === 'exception') { |
193
|
1 |
|
throw new FileNotFoundException($msg); |
194
|
|
|
|
195
|
1 |
|
} elseif ($config['missingAsset'] === 'notice') { |
196
|
1 |
|
trigger_error($msg, E_USER_NOTICE); |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
} |
200
|
|
|
|
201
|
1 |
|
return [$relativePath, $absolutePath, $wwwDir]; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* @param mixed $assetsVersions |
207
|
|
|
* @param string $absolutePath |
208
|
|
|
* @param array $config |
209
|
|
|
* @return mixed|string |
210
|
|
|
*/ |
211
|
1 |
|
private static function getAssetVersion($assetsVersions, $absolutePath, array $config) |
212
|
|
|
{ |
213
|
|
|
// Versions can be array or path to JSON file |
214
|
1 |
|
if ( ! is_array($assetsVersions)) { |
215
|
1 |
|
if ( ! file_exists($assetsVersions)) { |
216
|
1 |
|
throw new FileNotFoundException(sprintf("Asset versions file not found: '%s'.", $assetsVersions)); |
217
|
|
|
} |
218
|
1 |
|
$assetsVersions = Json::decode(file_get_contents($assetsVersions), Json::FORCE_ARRAY); |
219
|
|
|
} |
220
|
|
|
|
221
|
1 |
|
foreach ($assetsVersions as $path => $hash) { |
222
|
|
|
// Test if path from version file (may be relative) is in asset path |
223
|
1 |
|
if (Strings::endsWith($absolutePath, $path)) { |
224
|
1 |
|
return $hash; |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
|
228
|
1 |
|
$msg = sprintf("Asset macro: version of asset '%s' not found.", $absolutePath); |
229
|
1 |
|
switch ($config['missingVersion']) { |
230
|
1 |
|
case 'exception': |
231
|
1 |
|
throw new AssetVersionNotFound($msg); |
232
|
1 |
|
case 'notice': |
233
|
1 |
|
trigger_error($msg, E_USER_NOTICE); |
234
|
|
|
// no break |
235
|
|
|
default: |
236
|
1 |
|
return 'unknown'; |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* @param string $absolutePath |
243
|
|
|
* @param string $wwwDir |
244
|
|
|
* @param array $paths |
245
|
|
|
* @return string |
246
|
|
|
*/ |
247
|
1 |
|
private static function autodetectVersions($absolutePath, $wwwDir, array $paths) |
248
|
|
|
{ |
249
|
|
|
// Iterate over parent directories (stop in www dir) |
250
|
1 |
|
$dir = dirname($absolutePath); |
251
|
1 |
|
while (Strings::startsWith($dir, $wwwDir)) { |
252
|
1 |
|
foreach ($paths as $path) { |
253
|
1 |
|
$path = $dir . DIRECTORY_SEPARATOR . $path; |
254
|
1 |
|
if (file_exists($path)) { |
255
|
1 |
|
return $path; |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
// Get parent directory |
260
|
1 |
|
$dir = dirname($dir); |
261
|
|
|
} |
262
|
|
|
|
263
|
1 |
|
throw new FileNotFoundException( |
264
|
1 |
|
sprintf("None of the version files (%s) can be found in '%s' and parent directories up to www dir '%s'. " . |
265
|
1 |
|
"Create one of these files or set 'versions' in configuration.", |
266
|
1 |
|
implode(', ', $paths), |
267
|
1 |
|
dirname($absolutePath), |
268
|
1 |
|
$wwwDir |
269
|
|
|
) |
270
|
|
|
); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
} |
274
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.