Completed
Branch master (823b8c)
by Michal
03:50 queued 02:20
created

AssetMacro::getManifest()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 13
cts 13
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 13
nc 4
nop 4
crap 4
1
<?php
2
3
namespace Webrouse\AssetMacro;
4
5
use Latte;
6
use Latte\Macros\MacroSet;
7
use Nette\Caching\Cache;
8
use Nette\Caching\IStorage;
9
use Nette\Utils\Json;
10
use Nette\Utils\Strings;
11
use Nette\Utils\Validators;
12
use Webrouse\AssetMacro\Exceptions\AssetNotFoundException;
13
use Webrouse\AssetMacro\Exceptions\RevisionNotFound;
14
use Webrouse\AssetMacro\Exceptions\InvalidVariableException;
15
use Webrouse\AssetMacro\Exceptions\ManifestNotFoundException;
16
17
18 1
class AssetMacro extends MacroSet
19
{
20
21
	/**
22
	 * Name of Latte provider of macro configuration
23
	 */
24
	const CONFIG_PROVIDER = 'assetMacroConfig';
25
26
	/**
27
	 * Memory cache for decoded JSON content of revisions manifests (path => content)
28
	 * @var array
29
	 */
30
	private static $manifestCache = [];
31
32
33
	/**
34
	 * @param Latte\Compiler $compiler
35
	 */
36 1
	public static function install(Latte\Compiler $compiler)
37
	{
38 1
		$me = new self($compiler);
39 1
		$me->addMacro('asset', [$me, 'macroAsset']);
40 1
	}
41
42
	/**
43
	 * @param Latte\MacroNode $node
44
	 * @param Latte\PhpWriter $writer
45
	 * @return string
46
	 * @throws Latte\CompileException
47
	 */
48 1
	public function macroAsset(Latte\MacroNode $node, Latte\PhpWriter $writer)
49
	{
50 1
		if ($node->modifiers && $node->modifiers != '|noescape') {
51
			throw new Latte\CompileException('Only \'noescape\' modifier is allowed in ' . $node->getNotation());
52
		}
53
54
		// Validate arguments count
55 1
		$args = trim($node->args);
56 1
		$argsCount = $args === '' ? 0 : (substr_count($args, ',') + 1);
57 1
		if ($argsCount === 0) {
58 1
			throw new Latte\CompileException("Asset macro requires at least one argument.");
59 1
		} else if ($argsCount > 3) {
60 1
			throw new Latte\CompileException("Asset macro must have no more than 3 arguments.");
61
		}
62
63 1
		return $writer->write(
64 1
			'echo ' . ($node->modifiers !== '|noescape' ? '%escape' : '') .
65 1
			'(' . self::class . '::getOutput(' .
66 1
			'%node.word, ' .
67 1
			'%node.array, ' .
68 1
			'$basePath, ' .
69 1
			'$this->global->' . self::CONFIG_PROVIDER . ', ' .
70 1
			'isset($this->global->cacheStorage) ? $this->global->cacheStorage : null))');
71
	}
72
73
	/**
74
	 * @param string $asset      Asset relative path
75
	 * @param array $args        Other macro arguments
76
	 * @param string $basePath   Base path
77
	 * @param array $config      Macro configuration
78
	 * @param IStorage $storage  Cache storage
79
	 * @return string
80
	 */
81 1
	public static function getOutput($asset, array $args, $basePath, array $config, IStorage $storage = null)
82
	{
83 1
		$cacheKey = md5(implode(';', [$asset, $basePath, serialize($args), serialize($config)]));
84 1
		$cache = ($config['cache'] && $storage) ? new Cache($storage, 'Webrouse.AssetMacro') : null;
85
86
		// Load cached value
87 1
		if ($cache && ($output = $cache->load($cacheKey)) !== NULL) {
88 1
			return $output;
89
		}
90
91
		// Generate output and store value to cache
92 1
		$output = self::generateOutput($asset, $args, $basePath, $config);
93 1
		if ($cache) {
94 1
			$cache->save($cacheKey, $output);
95
		}
96
97 1
		return $output;
98
	}
99
100
	/**
101
	 * @param string $asset     Asset relative path
102
	 * @param array $args       Other macro arguments
103
	 * @param string $basePath  Base path
104
	 * @param array $config     Macro configuration
105
	 * @return string
106
	 */
107 1
	public static function generateOutput($asset, array $args, $basePath, array $config)
108
	{
109 1
		list($relativePath, $format, $needed) = self::processArguments($asset, $args);
110 1
		list($revision, $isVersion, $absolutePath) = self::getRevision($relativePath, $needed, $config);
111
112 1
		if (!file_exists($absolutePath)) {
113 1
			Utils::throwError(
114 1
				new AssetNotFoundException(sprintf("Asset '%s' not found.", $absolutePath)),
115 1
				$config['missingAsset'],
116 1
				$needed
117
			);
118 1
			return '';
119
		}
120
121 1
		return self::formatOutput($format, $absolutePath, $relativePath, $basePath, $revision, $isVersion);
122
	}
123
124
125
	/**
126
	 * @param string $asset  Asset path specified in macro
127
	 * @param array $args    Macro arguments
128
	 * @return array
129
	 */
130 1
	private static function processArguments($asset, array $args)
131
	{
132 1
		$format = isset($args['format']) ? $args['format'] : (isset($args[0]) ? $args[0] : '%url%');
133 1
		$needed = isset($args['need']) ? $args['need'] : (isset($args[1]) ? $args[1] : TRUE);
134
135 1
		Validators::assert($asset, 'string', 'path');
136 1
		Validators::assert($format, 'string', 'format');
137 1
		Validators::assert($needed, 'bool', 'need');
138
139 1
		$relativePath = Utils::normalizePath($asset);
140
141 1
		return [$relativePath, $format, $needed];
142
	}
143
144
	/**
145
	 * @param string $relativePath  Relative asset path
146
	 * @param string $needed        Fail if manifest doesn't exist?
147
	 * @param array $config         Macro configuration
148
	 * @return array
149
	 */
150 1
	private static function getRevision($relativePath, $needed, array $config)
151
	{
152 1
		$wwwDir = Utils::normalizePath($config['wwwDir']);
153 1
		$manifest = self::getManifest($relativePath, $needed, $wwwDir, $config);
154 1
		$revision = $manifest && isset($manifest[$relativePath]) ? $manifest[$relativePath] : null;
155
156
		// Throw error if revision not found in manifest
157 1
		if ($manifest && $revision === null) {
158 1
			Utils::throwError(
159 1
				new RevisionNotFound(sprintf("Revision for asset '%s' not found in manifest.", $relativePath)),
160 1
				$config['missingRevision'],
161 1
				$needed
0 ignored issues
show
Documentation introduced by
$needed is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
162
			);
163
		}
164
165
		// Is revision only version (query parameter) or full path to asset?
166 1
		$isVersion = $revision === null || !Strings::match($revision, '/[.\/]/');
167
168
		// Check if asset exists
169 1
		$filePath = $isVersion ?
170 1
			($wwwDir . DIRECTORY_SEPARATOR . $relativePath) :
171 1
			($wwwDir . DIRECTORY_SEPARATOR . Utils::normalizePath($revision));
172
173 1
		return [$revision, $isVersion, $filePath];
174
	}
175
176
177
	/**
178
	 * @param string $asset   Asset path specified in macro
179
	 * @param bool $needed    Fail if manifest doesn't exist?
180
	 * @param string $wwwDir  Public www dir
181
	 * @param array $config   Macro configuration
182
	 * @return null|array
183
	 */
184 1
	private static function getManifest($asset, $needed, $wwwDir, array $config)
185
	{
186 1
		$manifest = $config['manifest'];
187
188
		// Asset revisions specified directly in configuration
189 1
		if (is_array($manifest)) {
190 1
			return $manifest;
191
		}
192
193
		// Path to JSON manifest
194 1
		if (is_string($manifest)) {
195 1
			if (!file_exists($manifest)) {
196 1
				Utils::throwError(
197 1
					new ManifestNotFoundException(sprintf("Manifest file not found: '%s'.", $manifest)),
198 1
					$config['missingManifest'],
199 1
					$needed
200
				);
201 1
				return null;
202
			}
203 1
			return Json::decode(file_get_contents($manifest), Json::FORCE_ARRAY);
204
		}
205
206
		// Autodetect manifest path
207 1
		return self::autodetectManifest($asset, $wwwDir, $needed, $config);
208
	}
209
210
211
	/**
212
	 * @param string $asset   Asset path specified in macro
213
	 * @param string $wwwDir  Public www dir
214
	 * @param bool $needed    Fail if asset/manifest doesn't exist?
215
	 * @param array $config   Macro configuration
216
	 * @return null|array
217
	 */
218 1
	private static function autodetectManifest($asset, $wwwDir, $needed, array $config)
219
	{
220
		// Finding a manifest begins in the asset directory
221 1
		$dir = $wwwDir . DIRECTORY_SEPARATOR . Utils::normalizePath(dirname($asset));
222
223
		// Autodetect manifest
224 1
		$autodetectPaths = $config['autodetect'];
225 1
		while (Strings::startsWith($dir, $wwwDir)) {
226 1
			foreach ($autodetectPaths as $path) {
227 1
				$path = $dir . DIRECTORY_SEPARATOR . $path;
228 1
				if (file_exists($path)) {
229 1
					if (!isset(self::$manifestCache[$path])) {
230 1
						self::$manifestCache[$path] = Json::decode(file_get_contents($path), Json::FORCE_ARRAY);
231
					}
232 1
					return self::$manifestCache[$path];
233
				}
234
			}
235
236 1
			$dir = dirname($dir); // go up ../
237
		}
238
239 1
		Utils::throwError(
240 1
			new ManifestNotFoundException(sprintf("Manifest not found in: %s.", implode(', ', $autodetectPaths))),
241 1
			$config['missingManifest'],
242 1
			$needed
243
		);
244 1
		return null;
245
	}
246
247
	/**
248
	 * @param string $format           Output format
249
	 * @param string $absolutePath     Absolute asset path
250
	 * @param string $relativePath     Asset relative path
251
	 * @param string $basePath         Base path
252
	 * @param string|null $revision    Asset revision (version or path to file)
253
	 * @param bool $revisionIsVersion  Is revision only version or full path?
254
	 * @return string
255
	 */
256
	private static function formatOutput($format, $absolutePath, $relativePath, $basePath, $revision, $revisionIsVersion)
257
	{
258 1
		$revision = $revision ?: 'unknown';
259 1
		$relativePath = $revisionIsVersion ? $relativePath : $revision;
260
261 1
		return Strings::replace($format,
262 1
			'/%([^%]+)%/',
263 1
			function ($matches) use ($format, $absolutePath, $relativePath, $basePath, $revision, $revisionIsVersion) {
264 1
				switch ($matches[1]) {
265 1
					case 'content':
266 1
						return trim(file_get_contents($absolutePath));
267 1
					case 'raw':
268 1
						return $revision;
269 1
					case 'basePath':
270 1
						return $basePath;
271 1
					case 'path':
272 1
						return $relativePath;
273 1
					case 'url':
274 1
						return $revisionIsVersion ?
275 1
							sprintf("%s/%s?v=%s", $basePath, $relativePath, $revision) :
276 1
							sprintf("%s/%s", $basePath, $relativePath);
277
					default:
278 1
						$msg = sprintf(
279
							"Asset macro: Invalid variable '%s' in format '%s'. " .
280 1
							"Use one of allowed variables: %%raw%%, %%basePath%%, %%path%%, %%url%%.",
281 1
							$matches[1],
282 1
							$format
283
						);
284 1
						throw new InvalidVariableException($msg);
285
				}
286 1
			});
287
	}
288
}
289