Completed
Push — master ( 780b15...8349c6 )
by Michal
03:01
created

AssetMacro::macroAsset()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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