Skip to content

Commit

Permalink
Added output caching and %content% format placeholder. (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaljurecko committed Sep 21, 2017
1 parent 99591ff commit 823b8c4
Show file tree
Hide file tree
Showing 14 changed files with 533 additions and 181 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -72,7 +72,7 @@ assetMacro:
# Action if missing manifest file: exception, notice, or ignore
missingManifest: notice
# Action if missing asset revision in manifest: exception, notice, or ignore
missingRevision: ignore
missingRevision: notice
```
## Revision manifest

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -17,7 +17,8 @@
},
"require-dev": {
"nette/tester": "^2.0",
"nette/nette": ">=2.3"
"nette/nette": ">=2.3",
"mockery/mockery": "^0.9.9"
},
"autoload": {
"psr-0": {
Expand Down
252 changes: 142 additions & 110 deletions src/Webrouse/AssetMacro/AssetMacro.php
Expand Up @@ -4,6 +4,8 @@

use Latte;
use Latte\Macros\MacroSet;
use Nette\Caching\Cache;
use Nette\Caching\IStorage;
use Nette\Utils\Json;
use Nette\Utils\Strings;
use Nette\Utils\Validators;
Expand All @@ -16,10 +18,18 @@
class AssetMacro extends MacroSet
{

/**
* Name of Latte provider of macro configuration
*/
const CONFIG_PROVIDER = 'assetMacroConfig';

/**
* Memory cache for decoded JSON content of revisions manifests (path => content)
* @var array
*/
private static $manifestCache = [];


/**
* @param Latte\Compiler $compiler
*/
Expand All @@ -29,7 +39,6 @@ public static function install(Latte\Compiler $compiler)
$me->addMacro('asset', [$me, 'macroAsset']);
}


/**
* @param Latte\MacroNode $node
* @param Latte\PhpWriter $writer
Expand All @@ -38,22 +47,55 @@ public static function install(Latte\Compiler $compiler)
*/
public function macroAsset(Latte\MacroNode $node, Latte\PhpWriter $writer)
{
$args = trim($node->args);
if ($node->modifiers && $node->modifiers != '|noescape') {
throw new Latte\CompileException('Only \'noescape\' modifier is allowed in ' . $node->getNotation());
}

// Validate arguments count
$args = trim($node->args);
$argsCount = $args === '' ? 0 : (substr_count($args, ',') + 1);
if ($argsCount === 0) {
throw new Latte\CompileException("Asset macro requires at least one argument.");
}
if ($argsCount > 3) {
} else if ($argsCount > 3) {
throw new Latte\CompileException("Asset macro must have no more than 3 arguments.");
}

return $writer->write(
'echo %escape(' . self::class . '::resolveAssetPath(' .
'%node.word, %node.array, $basePath, $this->global->' . self::CONFIG_PROVIDER . '))');
'echo ' . ($node->modifiers !== '|noescape' ? '%escape' : '') .
'(' . self::class . '::getOutput(' .
'%node.word, ' .
'%node.array, ' .
'$basePath, ' .
'$this->global->' . self::CONFIG_PROVIDER . ', ' .
'isset($this->global->cacheStorage) ? $this->global->cacheStorage : null))');
}

/**
* @param string $asset Asset relative path
* @param array $args Other macro arguments
* @param string $basePath Base path
* @param array $config Macro configuration
* @param IStorage $storage Cache storage
* @return string
*/
public static function getOutput($asset, array $args, $basePath, array $config, IStorage $storage = null)
{
$cacheKey = md5(implode(';', [$asset, $basePath, serialize($args), serialize($config)]));
$cache = ($config['cache'] && $storage) ? new Cache($storage, 'Webrouse.AssetMacro') : null;

// Load cached value
if ($cache && ($output = $cache->load($cacheKey)) !== NULL) {
return $output;
}

// Generate output and store value to cache
$output = self::generateOutput($asset, $args, $basePath, $config);
if ($cache) {
$cache->save($cacheKey, $output);
}

return $output;
}

/**
* @param string $asset Asset relative path
Expand All @@ -62,72 +104,21 @@ public function macroAsset(Latte\MacroNode $node, Latte\PhpWriter $writer)
* @param array $config Macro configuration
* @return string
*/
public static function resolveAssetPath($asset, array $args, $basePath, array $config)
public static function generateOutput($asset, array $args, $basePath, array $config)
{
list($path, $format, $need) = self::processArguments($asset, $args);
$wwwDir = Utils::normalizePath($config['wwwDir']);
$manifest = self::resolveManifest($path, $need, $wwwDir, $config);
$revision = $manifest === NULL ? NULL : self::resolveRevision($manifest, $path, $need, $config);
list($relativePath, $format, $needed) = self::processArguments($asset, $args);
list($revision, $isVersion, $absolutePath) = self::getRevision($relativePath, $needed, $config);

// Is revision only version (query parameter) or full path to asset?
$revisionIsVersion = $revision === NULL || ! Strings::match($revision, '/[.\/]/');

// Check if asset exists
$ds = DIRECTORY_SEPARATOR;
$filePath = $revisionIsVersion ?
($wwwDir . $ds . $path) :
($wwwDir . $ds . Utils::normalizePath($revision));
if ( ! file_exists($filePath)) {
if (!file_exists($absolutePath)) {
Utils::throwError(
new AssetNotFoundException(sprintf("Asset '%s' not found.", $filePath)),
new AssetNotFoundException(sprintf("Asset '%s' not found.", $absolutePath)),
$config['missingAsset'],
$need
$needed
);
return '';
}

// Format output
return self::formatOutput($format, $basePath, $path, $revision, $revisionIsVersion);
}


/**
* @param string $format Output format
* @param string $basePath Base path
* @param string $path Asset relative path
* @param string|null $revision Asset revision (version or path to file)
* @param bool $revisionIsVersion Is revision only version or full path?
* @return string
*/
private static function formatOutput($format, $basePath, $path, $revision, $revisionIsVersion)
{
$revision = $revision ?: 'unknown';
$path = $revisionIsVersion ? $path : $revision;

return Strings::replace($format,
'/%([^%]+)%/',
function ($matches) use ($basePath, $format, $path, $revision, $revisionIsVersion) {
switch ($matches[1]) {
case 'raw':
return $revision;
case 'basePath':
return $basePath;
case 'path':
return $path;
case 'url':
return $revisionIsVersion ?
sprintf("%s/%s?v=%s", $basePath, $path, $revision) :
sprintf("%s/%s", $basePath, $path);
default:
$msg = sprintf(
"Asset macro: Invalid variable '%s' in format '%s'. " .
"Use one of allowed variables: %%raw%%, %%basePath%%, %%path%%, %%url%%.",
$matches[1],
$format
);
throw new InvalidVariableException($msg);
}
});
return self::formatOutput($format, $absolutePath, $relativePath, $basePath, $revision, $isVersion);
}


Expand All @@ -139,26 +130,58 @@ function ($matches) use ($basePath, $format, $path, $revision, $revisionIsVersio
private static function processArguments($asset, array $args)
{
$format = isset($args['format']) ? $args['format'] : (isset($args[0]) ? $args[0] : '%url%');
$need = isset($args['need']) ? $args['need'] : (isset($args[1]) ? $args[1] : TRUE);
$needed = isset($args['need']) ? $args['need'] : (isset($args[1]) ? $args[1] : TRUE);

Validators::assert($asset, 'string', 'path');
Validators::assert($format, 'string', 'format');
Validators::assert($need, 'bool', 'need');
Validators::assert($needed, 'bool', 'need');

$path = Utils::normalizePath($asset);
$relativePath = Utils::normalizePath($asset);

return [$path, $format, $need];
return [$relativePath, $format, $needed];
}

/**
* @param string $relativePath Relative asset path
* @param string $needed Fail if manifest doesn't exist?
* @param array $config Macro configuration
* @return array
*/
private static function getRevision($relativePath, $needed, array $config)
{
$wwwDir = Utils::normalizePath($config['wwwDir']);
$manifest = self::getManifest($relativePath, $needed, $wwwDir, $config);
$revision = $manifest && isset($manifest[$relativePath]) ? $manifest[$relativePath] : null;

// Throw error if revision not found in manifest
if ($manifest && $revision === null) {
Utils::throwError(
new RevisionNotFound(sprintf("Revision for asset '%s' not found in manifest.", $relativePath)),
$config['missingRevision'],
$needed
);
}

// Is revision only version (query parameter) or full path to asset?
$isVersion = $revision === null || !Strings::match($revision, '/[.\/]/');

// Check if asset exists
$filePath = $isVersion ?
($wwwDir . DIRECTORY_SEPARATOR . $relativePath) :
($wwwDir . DIRECTORY_SEPARATOR . Utils::normalizePath($revision));

return [$revision, $isVersion, $filePath];
}


/**
* @param string $asset Asset path specified in macro
* @param bool $need Fail if manifest doesn't exist?
* @param bool $needed Fail if manifest doesn't exist?
* @param string $wwwDir Public www dir
* @param array $config Macro configuration
* @return null|array
*/
private static function resolveManifest($asset, $need, $wwwDir, array $config)
private static function getManifest($asset, $needed, $wwwDir, array $config)
{
$manifest = $config['manifest'];

Expand All @@ -169,30 +192,30 @@ private static function resolveManifest($asset, $need, $wwwDir, array $config)

// Path to JSON manifest
if (is_string($manifest)) {
if ( ! file_exists($manifest)) {
if (!file_exists($manifest)) {
Utils::throwError(
new ManifestNotFoundException(sprintf("Manifest file not found: '%s'.", $manifest)),
$config['missingManifest'],
$need
$needed
);
return NULL;
return null;
}
return Json::decode(file_get_contents($manifest), Json::FORCE_ARRAY);
}

// Autodetect manifest path
return self::autodetectManifest($asset, $wwwDir, $need, $config);
return self::autodetectManifest($asset, $wwwDir, $needed, $config);
}


/**
* @param string $asset Asset path specified in macro
* @param string $wwwDir Public www dir
* @param bool $need Fail if asset/manifest doesn't exist?
* @param bool $needed Fail if asset/manifest doesn't exist?
* @param array $config Macro configuration
* @return null|array
*/
private static function autodetectManifest($asset, $wwwDir, $need, array $config)
private static function autodetectManifest($asset, $wwwDir, $needed, array $config)
{
// Finding a manifest begins in the asset directory
$dir = $wwwDir . DIRECTORY_SEPARATOR . Utils::normalizePath(dirname($asset));
Expand All @@ -203,54 +226,63 @@ private static function autodetectManifest($asset, $wwwDir, $need, array $config
foreach ($autodetectPaths as $path) {
$path = $dir . DIRECTORY_SEPARATOR . $path;
if (file_exists($path)) {
return self::getManifest($path);
if (!isset(self::$manifestCache[$path])) {
self::$manifestCache[$path] = Json::decode(file_get_contents($path), Json::FORCE_ARRAY);
}
return self::$manifestCache[$path];
}
}

$dir = dirname($dir); // go up
$dir = dirname($dir); // go up ../
}

Utils::throwError(
new ManifestNotFoundException(sprintf("Manifest not found in: %s.", implode(', ', $autodetectPaths))),
$config['missingManifest'],
$need
$needed
);
return NULL;
return null;
}

/**
* Get manifest content and cache it
* @param string $path
* @return array
*/
private static function getManifest($path) {
if (!isset(self::$manifestCache[$path])) {
self::$manifestCache[$path] = Json::decode(file_get_contents($path), Json::FORCE_ARRAY);
}
return self::$manifestCache[$path];
}


/**
* @param null|array $manifest Array of revisions
* @param string $path Asset path
* @param bool $need Fail if revision doesn't exist?
* @param array $config Macro configuration
* @return null|string
* @param string $format Output format
* @param string $absolutePath Absolute asset path
* @param string $relativePath Asset relative path
* @param string $basePath Base path
* @param string|null $revision Asset revision (version or path to file)
* @param bool $revisionIsVersion Is revision only version or full path?
* @return string
*/
private static function resolveRevision($manifest, $path, $need, array $config)
private static function formatOutput($format, $absolutePath, $relativePath, $basePath, $revision, $revisionIsVersion)
{
$revision = isset($manifest[$path]) ? $manifest[$path] : NULL;

if ($revision === NULL) {
Utils::throwError(
new RevisionNotFound(sprintf("Revision for asset '%s' not found in manifest.", $path)),
$config['missingRevision'],
$need
);
}
$revision = $revision ?: 'unknown';
$relativePath = $revisionIsVersion ? $relativePath : $revision;

return $revision;
return Strings::replace($format,
'/%([^%]+)%/',
function ($matches) use ($format, $absolutePath, $relativePath, $basePath, $revision, $revisionIsVersion) {
switch ($matches[1]) {
case 'content':
return trim(file_get_contents($absolutePath));
case 'raw':
return $revision;
case 'basePath':
return $basePath;
case 'path':
return $relativePath;
case 'url':
return $revisionIsVersion ?
sprintf("%s/%s?v=%s", $basePath, $relativePath, $revision) :
sprintf("%s/%s", $basePath, $relativePath);
default:
$msg = sprintf(
"Asset macro: Invalid variable '%s' in format '%s'. " .
"Use one of allowed variables: %%raw%%, %%basePath%%, %%path%%, %%url%%.",
$matches[1],
$format
);
throw new InvalidVariableException($msg);
}
});
}

}

0 comments on commit 823b8c4

Please sign in to comment.