Completed
Push — master ( a173a0...69eb71 )
by Michal
07:38
created

AssetMacro   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 178
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 11

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 34
c 1
b 0
f 0
lcom 0
cbo 11
dl 0
loc 178
ccs 83
cts 83
cp 1
rs 9.2

5 Methods

Rating   Name   Duplication   Size   Complexity  
A install() 0 5 1
A macroAsset() 0 21 4
C generateAssetPath() 0 60 17
C getAssetVersion() 0 28 8
B autodetectVersions() 0 25 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 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 1
class AssetMacro extends MacroSet
16
{
17
	const VERSIONS_AUTODETECT = NULL;
18
19
	const CONFIG_PROVIDER = 'assetMacroConfig';
20
21
	/**
22
	 * @param Latte\Compiler $compiler
23
	 */
24 1
	public static function install(Latte\Compiler $compiler)
25
	{
26 1
		$me = new self($compiler);
27 1
		$me->addMacro('asset', [$me, 'macroAsset']);
28 1
	}
29
30
	/**
31
	 * @param Latte\MacroNode $node
32
	 * @param Latte\PhpWriter $writer
33
	 * @return string
34
	 * @throws Latte\CompileException
35
	 */
36 1
	public function macroAsset(Latte\MacroNode $node, Latte\PhpWriter $writer)
37
	{
38 1
		$args = trim($node->args);
39 1
		$argsCount = $args === '' ? 0 : (substr_count($args, ',') + 1);
40
41
		// Validate arguments count
42 1
		if ($argsCount === 0) {
43 1
			throw new Latte\CompileException("Asset macro requires at least one argument.");
44
		}
45 1
		if ($argsCount > 3) {
46 1
			throw new Latte\CompileException("Asset macro must have no more than 3 arguments.");
47
		}
48
49
		$content =
50
			self::class . '::generateAssetPath(' .
51
			'%node.array, ' .
52
			'$basePath, ' .
53 1
			'$template->global->' . self::CONFIG_PROVIDER . ')';
54
55 1
		return $writer->write('echo %escape(' . $content . ')');
56
	}
57
58
	/**
59
	 * @param array $args
60
	 * @param string $basePath
61
	 * @param array $config
62
	 * @return string
63
	 */
64 1
	public static function generateAssetPath(array $args, $basePath, $config)
65
	{
66
		// Arguments
67 1
		$relativePath = $args[0];
68 1
		$format = isset($args[1]) ? $args[1] : "%url%";
69 1
		$needed = isset($args[2]) ? $args[2] : TRUE;
70
71
		// Validate arguments
72 1
		if (!is_string($relativePath)) {
73 1
			throw new InvalidArgumentException(sprintf("Asset macro: Invalid type of the first argument. Required string. Given %s.", gettype($relativePath)));
74
		}
75 1
		if (!is_string($format)) {
76 1
			throw new InvalidArgumentException(sprintf("Asset macro: Invalid type of the second argument. Required string. Given %s.", gettype($format)));
77
		}
78 1
		if (!is_bool($needed)) {
79 1
			throw new InvalidArgumentException(sprintf("Asset macro: Invalid type of the third argument. Required boolean. Given %s.", gettype($needed)));
80
		}
81
82
		// Validate paths
83 1
		$relativePath = ltrim($relativePath, '\\/');
84 1
		if (($wwwDir = realpath($config['wwwDir'])) === FALSE) {
85 1
			throw new DirNotFoundException(sprintf("Www dir '%s' not found.", $config['wwwDir']));
86
		}
87 1
		if (($absolutePath = realpath($wwwDir . DIRECTORY_SEPARATOR . $relativePath)) === FALSE) {
88 1
			if ($needed) {
89 1
				$msg = sprintf("Asset '%s' not found.", $relativePath);
90 1
				if ($config['missingAsset'] === 'exception') {
91 1
					throw new FileNotFoundException($msg);
92 1
				} elseif ($config['missingAsset'] === 'notice') {
93 1
					trigger_error($msg, E_USER_NOTICE);
94
				}
95
			}
96 1
			return '';
97
		}
98
99
		// Get asset version
100 1
		$versions = $config['versions'];
101 1
		if ($versions === self::VERSIONS_AUTODETECT) {
102 1
			$versions = self::autodetectVersions($absolutePath, $wwwDir, $config['autodetectPaths']);
103
		}
104 1
		$version = self::getAssetVersion($versions, $absolutePath, $config);
105
106
		// Generate output according format argument
107 1
		return Strings::replace($format, '/%([^%]+)%/', function ($matches) use ($basePath, $format, $relativePath, $version) {
108 1
			switch ($matches[1]) {
109 1
				case 'url':
110 1
					return sprintf("%s/%s?v=%s", $basePath, $relativePath, $version);
111 1
				case 'version':
112 1
					return $version;
113 1
				case 'basePath':
114 1
					return $basePath;
115 1
				case 'dir':
116 1
					return dirname($relativePath);
117 1
				case 'file':
118 1
					return basename($relativePath);
119
				default:
120 1
					throw new InvalidVariableException(sprintf("Asset macro: Invalid variable '%s' in format '%s'. Use one of allowed variables: %%url%%, %%version%%, %%basePath%%, %%dir%%, %%file%%.", $matches[1], $format));
121
			}
122 1
		});
123
	}
124
125
	/**
126
	 * @param mixed $assetsVersions
127
	 * @param string $absolutePath
128
	 * @param array $config
129
	 * @return mixed|string
130
	 */
131 1
	private static function getAssetVersion($assetsVersions, $absolutePath, array $config)
132
	{
133
		// Versions can be array or path to JSON file
134 1
		if (!is_array($assetsVersions)) {
135 1
			if (!file_exists($assetsVersions)) {
136 1
				throw new FileNotFoundException(sprintf("Asset versions file not found: '%s'.", $assetsVersions));
137
			}
138 1
			$assetsVersions = Json::decode(file_get_contents($assetsVersions), Json::FORCE_ARRAY);
139
		}
140
141 1
		foreach ($assetsVersions as $path => $hash) {
142
			// Test if path from version file (may be relative) is in asset path
143 1
			if (Strings::endsWith($absolutePath, $path)) {
144 1
				return $hash;
145
			}
146
		}
147
148 1
		$msg = sprintf("Asset macro: version of asset '%s' not found.", $absolutePath);
149 1
		switch ($config['missingVersion']) {
150 1
			case 'exception':
151 1
				throw new AssetVersionNotFound($msg);
152 1
			case 'notice':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
153 1
				trigger_error($msg, E_USER_NOTICE);
154 1
			case 'ignore':
155
		}
156
157 1
		return 'unknown';
158
	}
159
160
161
	/**
162
	 * @param string $absolutePath
163
	 * @param string $wwwDir
164
	 * @param array $paths
165
	 * @return string
166
	 */
167 1
	private static function autodetectVersions($absolutePath, $wwwDir, array $paths)
168
	{
169
		// Iterate over parent directories (stop in www dir)
170 1
		$dir = dirname($absolutePath);
171 1
		while (Strings::startsWith($dir, $wwwDir)) {
172 1
			foreach ($paths as $path) {
173 1
				$path = $dir . DIRECTORY_SEPARATOR . $path;
174 1
				if (file_exists($path)) {
175 1
					return $path;
176
				}
177
			}
178
179
			// Get parent directory
180 1
			$dir = dirname($dir);
181
		}
182
183 1
		throw new FileNotFoundException(
184 1
			sprintf("None of the version files (%s) can be found in '%s' and parent directories up to www dir '%s'. " .
185 1
				"Create one of these files or set 'versions' in configuration.",
186 1
				implode(', ', $paths),
187 1
				dirname($absolutePath),
188 1
				$wwwDir
189
			)
190
		);
191
	}
192
}
193