Completed
Push — master ( 3980d1...e5ab6e )
by Michal
04:31
created

AssetMacro::autodetectManifest()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5.3906

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 27
ccs 12
cts 16
cp 0.75
rs 8.439
cc 5
eloc 16
nc 5
nop 4
crap 5.3906
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\DirNotFoundException;
13
use Webrouse\AssetMacro\Exceptions\InvalidVariableException;
14
use Webrouse\AssetMacro\Exceptions\ManifestNotFoundException;
15
16
17 1
class AssetMacro extends MacroSet
18
{
19
20
	const CONFIG_PROVIDER = 'assetMacroConfig';
21
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, $template->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
		$isVersion = $revision === NULL || strspn($revision, "./") === 0;
74
75
		// Check if asset exists
76 1
		if ($config['existCheck'] === TRUE) {
77 1
			$ds = DIRECTORY_SEPARATOR;
78 1
			$filePath = $isVersion ? ($wwwDir . $ds . $path) : ($wwwDir . $ds . Utils::normalizePath($revision));
79 1 View Code Duplication
			if ( ! file_exists($filePath)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
80 1
				$msg = sprintf("Asset '%s' not found.", $filePath);
81 1
				Utils::throwError(new AssetNotFoundException($msg), $config['missingAsset'], $need);
82 1
				return '';
83
			}
84
		}
85
86
		// Format output
87 1
		return self::formatOutput($format, $basePath, $path, $revision, $isVersion);
88
	}
89
90
91
	/**
92
	 * @param string $format         Output format
93
	 * @param string $basePath       Base path
94
	 * @param string $path           Asset relative path
95
	 * @param string|null $revision  Asset revision (version or path to file)
96
	 * @param bool   $isVersion      Is revision only version or full path?
97
	 * @return string
98
	 */
99
	private static function formatOutput($format, $basePath, $path, $revision, $isVersion)
100
	{
101 1
		$revision = $revision ?: 'unknown';
102 1
		$path = $isVersion ? $path : $revision;
103
104 1
		return Strings::replace($format,
105 1
			'/%([^%]+)%/',
106 1
			function ($matches) use ($basePath, $format, $path, $revision, $isVersion) {
107 1
				switch ($matches[1]) {
108 1
					case 'raw':
109 1
						return $revision;
110 1
					case 'basePath':
111 1
						return $basePath;
112 1
					case 'path':
113 1
						return $path;
114 1
					case 'url':
115 1
						return $isVersion ?
116 1
							sprintf("%s/%s?v=%s", $basePath, $path, $revision) :
117 1
							sprintf("%s/%s", $basePath, $path);
118
					default:
119 1
						$msg = sprintf(
120
							"Asset macro: Invalid variable '%s' in format '%s'. " .
121 1
							"Use one of allowed variables: %%raw%%, %%basePath%%, %%path%%, %%url%%.",
122 1
							$matches[1],
123 1
							$format
124
						);
125 1
						throw new InvalidVariableException($msg);
126
				}
127 1
			});
128
	}
129
130
131
	/**
132
	 * @param string $asset  Asset path specified in macro
133
	 * @param array $args    Macro arguments
134
	 * @return array
135
	 */
136 1
	private static function processArguments($asset, array $args)
137
	{
138 1
		$format = isset($args['format']) ? $args['format'] : (isset($args[0]) ? $args[0] : '%url%');
139 1
		$need = isset($args['need']) ? $args['need'] : (isset($args[1]) ? $args[1] : TRUE);
140
141 1
		Validators::assert($asset, 'string', 'path');
142 1
		Validators::assert($format, 'string', 'format');
143 1
		Validators::assert($need, 'bool', 'need');
144
145 1
		$path = Utils::normalizePath($asset);
146
147 1
		return [$path, $format, $need];
148
	}
149
150
151
	/**
152
	 * @param string $asset   Asset path specified in macro
153
	 * @param bool $need      Fail if manifest doesn't exist?
154
	 * @param string $wwwDir  Public www dir
155
	 * @param array $config   Macro configuration
156
	 * @return null|array
157
	 */
158 1
	private static function resolveManifest($asset, $need, $wwwDir, array $config)
159
	{
160 1
		$manifest = $config['manifest'];
161
162
		// Asset revisions specified directly in configuration
163 1
		if (is_array($manifest)) {
164 1
			return $manifest;
165
		}
166
167
		// Path to JSON manifest
168 1
		if (is_string($manifest)) {
169 1 View Code Duplication
			if ( ! file_exists($manifest)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
170 1
				$msg = sprintf("Manifest file not found: '%s'.", $manifest);
171 1
				Utils::throwError(new ManifestNotFoundException($msg), $config['missingManifest'], $need);
172 1
				return NULL;
173
			}
174 1
			return Json::decode(file_get_contents($manifest), Json::FORCE_ARRAY);
175
		}
176
177
		// Autodetect manifest path
178 1
		return self::autodetectManifest($asset, $wwwDir, $need, $config);
179
	}
180
181
182
	/**
183
	 * @param string $asset   Asset path specified in macro
184
	 * @param string $wwwDir  Public www dir
185
	 * @param bool $need      Fail if asset/manifest doesn't exist?
186
	 * @param array $config   Macro configuration
187
	 * @return null|array
188
	 */
189 1
	private static function autodetectManifest($asset, $wwwDir, $need, array $config)
190
	{
191
		// Get asset dir, there begins search manifest
192 1
		$dir = realpath($wwwDir . DIRECTORY_SEPARATOR . dirname($asset));
193 1
		if ($dir === FALSE) {
194
			$msg = sprintf("Parent dir of asset '%s' not found.", $asset);
195
			Utils::throwError(new DirNotFoundException($msg), $config['missingAsset'], $need);
196
			return NULL;
197
		}
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
		$msg = sprintf("Manifest not found in: %s.", implode(', ', $autodetectPaths));
213 1
		Utils::throwError(new ManifestNotFoundException($msg), $config['missingManifest'], $need);
214
		return NULL;
215
	}
216
217
218
	/**
219
	 * @param null|array $manifest  Array of revisions
220
	 * @param string $path          Asset path
221
	 * @param bool $need            Fail if revision doesn't exist?
222
	 * @param array $config         Macro configuration
223
	 * @return null|string
224
	 */
225 1
	private static function resolveRevision($manifest, $path, $need, array $config)
226
	{
227 1
		$revision = isset($manifest[$path]) ? $manifest[$path] : NULL;
228
229 1
		if ($revision === NULL) {
230 1
			$msg = sprintf("Revision for asset '%s' not found in manifest.", $path);
231 1
			Utils::throwError(new RevisionNotFound($msg), $config['missingRevision'], $need);
232
		}
233
234 1
		return $revision;
235
	}
236
237
}
238