Completed
Push — master ( 24e47f...3cbef2 )
by Michal
08:15
created

AssetMacro   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 12
dl 0
loc 241
ccs 99
cts 99
cp 1
rs 8.8
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
C formatOutput() 0 30 8
B processArguments() 0 13 5
B resolveManifest() 0 25 4
A install() 0 5 1
A macroAsset() 0 17 4
B resolveAssetPath() 0 27 5
B autodetectManifest() 0 25 4
A getManifest() 0 6 2
A resolveRevision() 0 14 3
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 Nette\Utils\Validators;
10
use Webrouse\AssetMacro\Exceptions\AssetNotFoundException;
11
use Webrouse\AssetMacro\Exceptions\RevisionNotFound;
12
use Webrouse\AssetMacro\Exceptions\InvalidVariableException;
13
use Webrouse\AssetMacro\Exceptions\ManifestNotFoundException;
14
15
16 1
class AssetMacro extends MacroSet
17
{
18
19
	const CONFIG_PROVIDER = 'assetMacroConfig';
20
21
	private static $manifestCache = [];
22
23
	/**
24
	 * @param Latte\Compiler $compiler
25
	 */
26 1
	public static function install(Latte\Compiler $compiler)
27
	{
28 1
		$me = new self($compiler);
29 1
		$me->addMacro('asset', [$me, 'macroAsset']);
30 1
	}
31
32
33
	/**
34
	 * @param Latte\MacroNode $node
35
	 * @param Latte\PhpWriter $writer
36
	 * @return string
37
	 * @throws Latte\CompileException
38
	 */
39 1
	public function macroAsset(Latte\MacroNode $node, Latte\PhpWriter $writer)
40
	{
41 1
		$args = trim($node->args);
42
43
		// Validate arguments count
44 1
		$argsCount = $args === '' ? 0 : (substr_count($args, ',') + 1);
45 1
		if ($argsCount === 0) {
46 1
			throw new Latte\CompileException("Asset macro requires at least one argument.");
47
		}
48 1
		if ($argsCount > 3) {
49 1
			throw new Latte\CompileException("Asset macro must have no more than 3 arguments.");
50
		}
51
52 1
		return $writer->write(
53
			'echo %escape(' . self::class . '::resolveAssetPath(' .
54 1
			'%node.word, %node.array, $basePath, $this->global->' . self::CONFIG_PROVIDER . '))');
55
	}
56
57
58
	/**
59
	 * @param string $asset     Asset relative path
60
	 * @param array $args       Other macro arguments
61
	 * @param string $basePath  Base path
62
	 * @param array $config     Macro configuration
63
	 * @return string
64
	 */
65 1
	public static function resolveAssetPath($asset, array $args, $basePath, array $config)
66
	{
67 1
		list($path, $format, $need) = self::processArguments($asset, $args);
68 1
		$wwwDir = Utils::normalizePath($config['wwwDir']);
69 1
		$manifest = self::resolveManifest($path, $need, $wwwDir, $config);
70 1
		$revision = $manifest === NULL ? NULL : self::resolveRevision($manifest, $path, $need, $config);
71
72
		// Is revision only version (query parameter) or full path to asset?
73 1
		$revisionIsVersion = $revision === NULL || ! Strings::match($revision, '/[.\/]/');
74
75
		// Check if asset exists
76 1
		$ds = DIRECTORY_SEPARATOR;
77 1
		$filePath = $revisionIsVersion ?
78 1
			($wwwDir . $ds . $path) :
79 1
			($wwwDir . $ds . Utils::normalizePath($revision));
80 1
		if ( ! file_exists($filePath)) {
81 1
			Utils::throwError(
82 1
				new AssetNotFoundException(sprintf("Asset '%s' not found.", $filePath)),
83 1
				$config['missingAsset'],
84 1
				$need
85
			);
86 1
			return '';
87
		}
88
89
		// Format output
90 1
		return self::formatOutput($format, $basePath, $path, $revision, $revisionIsVersion);
91
	}
92
93
94
	/**
95
	 * @param string $format         Output format
96
	 * @param string $basePath       Base path
97
	 * @param string $path           Asset relative path
98
	 * @param string|null $revision  Asset revision (version or path to file)
99
	 * @param bool   $revisionIsVersion      Is revision only version or full path?
100
	 * @return string
101
	 */
102
	private static function formatOutput($format, $basePath, $path, $revision, $revisionIsVersion)
103
	{
104 1
		$revision = $revision ?: 'unknown';
105 1
		$path = $revisionIsVersion ? $path : $revision;
106
107 1
		return Strings::replace($format,
108 1
			'/%([^%]+)%/',
109 1
			function ($matches) use ($basePath, $format, $path, $revision, $revisionIsVersion) {
110 1
				switch ($matches[1]) {
111 1
					case 'raw':
112 1
						return $revision;
113 1
					case 'basePath':
114 1
						return $basePath;
115 1
					case 'path':
116 1
						return $path;
117 1
					case 'url':
118 1
						return $revisionIsVersion ?
119 1
							sprintf("%s/%s?v=%s", $basePath, $path, $revision) :
120 1
							sprintf("%s/%s", $basePath, $path);
121
					default:
122 1
						$msg = sprintf(
123
							"Asset macro: Invalid variable '%s' in format '%s'. " .
124 1
							"Use one of allowed variables: %%raw%%, %%basePath%%, %%path%%, %%url%%.",
125 1
							$matches[1],
126 1
							$format
127
						);
128 1
						throw new InvalidVariableException($msg);
129
				}
130 1
			});
131
	}
132
133
134
	/**
135
	 * @param string $asset  Asset path specified in macro
136
	 * @param array $args    Macro arguments
137
	 * @return array
138
	 */
139 1
	private static function processArguments($asset, array $args)
140
	{
141 1
		$format = isset($args['format']) ? $args['format'] : (isset($args[0]) ? $args[0] : '%url%');
142 1
		$need = isset($args['need']) ? $args['need'] : (isset($args[1]) ? $args[1] : TRUE);
143
144 1
		Validators::assert($asset, 'string', 'path');
145 1
		Validators::assert($format, 'string', 'format');
146 1
		Validators::assert($need, 'bool', 'need');
147
148 1
		$path = Utils::normalizePath($asset);
149
150 1
		return [$path, $format, $need];
151
	}
152
153
154
	/**
155
	 * @param string $asset   Asset path specified in macro
156
	 * @param bool $need      Fail if manifest doesn't exist?
157
	 * @param string $wwwDir  Public www dir
158
	 * @param array $config   Macro configuration
159
	 * @return null|array
160
	 */
161 1
	private static function resolveManifest($asset, $need, $wwwDir, array $config)
162
	{
163 1
		$manifest = $config['manifest'];
164
165
		// Asset revisions specified directly in configuration
166 1
		if (is_array($manifest)) {
167 1
			return $manifest;
168
		}
169
170
		// Path to JSON manifest
171 1
		if (is_string($manifest)) {
172 1
			if ( ! file_exists($manifest)) {
173 1
				Utils::throwError(
174 1
					new ManifestNotFoundException(sprintf("Manifest file not found: '%s'.", $manifest)),
175 1
					$config['missingManifest'],
176 1
					$need
177
				);
178 1
				return NULL;
179
			}
180 1
			return Json::decode(file_get_contents($manifest), Json::FORCE_ARRAY);
181
		}
182
183
		// Autodetect manifest path
184 1
		return self::autodetectManifest($asset, $wwwDir, $need, $config);
185
	}
186
187
188
	/**
189
	 * @param string $asset   Asset path specified in macro
190
	 * @param string $wwwDir  Public www dir
191
	 * @param bool $need      Fail if asset/manifest doesn't exist?
192
	 * @param array $config   Macro configuration
193
	 * @return null|array
194
	 */
195 1
	private static function autodetectManifest($asset, $wwwDir, $need, array $config)
196
	{
197
		// Finding a manifest begins in the asset directory
198 1
		$dir = $wwwDir . DIRECTORY_SEPARATOR . Utils::normalizePath(dirname($asset));
199
200
		// Autodetect manifest
201 1
		$autodetectPaths = $config['autodetect'];
202 1
		while (Strings::startsWith($dir, $wwwDir)) {
203 1
			foreach ($autodetectPaths as $path) {
204 1
				$path = $dir . DIRECTORY_SEPARATOR . $path;
205 1
				if (file_exists($path)) {
206 1
					return self::getManifest($path);
207
				}
208
			}
209
210 1
			$dir = dirname($dir); // go up
211
		}
212
213 1
		Utils::throwError(
214 1
			new ManifestNotFoundException(sprintf("Manifest not found in: %s.", implode(', ', $autodetectPaths))),
215 1
			$config['missingManifest'],
216 1
			$need
217
		);
218 1
		return NULL;
219
	}
220
221
	/**
222
	 * Get manifest content and cache it
223
	 * @param string $path
224
	 * @return array
225
	 */
226
	private static function getManifest($path) {
227 1
		if (!isset(self::$manifestCache[$path])) {
228 1
			self::$manifestCache[$path] = Json::decode(file_get_contents($path), Json::FORCE_ARRAY);
229
		}
230 1
		return self::$manifestCache[$path];
231
	}
232
233
234
	/**
235
	 * @param null|array $manifest  Array of revisions
236
	 * @param string $path          Asset path
237
	 * @param bool $need            Fail if revision doesn't exist?
238
	 * @param array $config         Macro configuration
239
	 * @return null|string
240
	 */
241 1
	private static function resolveRevision($manifest, $path, $need, array $config)
242
	{
243 1
		$revision = isset($manifest[$path]) ? $manifest[$path] : NULL;
244
245 1
		if ($revision === NULL) {
246 1
			Utils::throwError(
247 1
				new RevisionNotFound(sprintf("Revision for asset '%s' not found in manifest.", $path)),
248 1
				$config['missingRevision'],
249 1
				$need
250
			);
251
		}
252
253 1
		return $revision;
254
	}
255
256
}
257