Resources.php
<?php
namespace Lia\Compo;
/**
* A component for managing multiple css & js files for a single request.
*
*
* @todo Use config for default site title
* @todo move SEO code into it's own component
* @todo Auto-add charset=utf8 & viewport meta tags (with to disable)
* @todo Add url prefix for compiled files as a config option
* @todo allow specifying sort priority when ADDING a file
* @todo better routing
* @todo Separate large files from the block of compiled code.
* @todo minify js & css
* @todo Create resources object that handles concatenation, so this class is merely an integration of said resource object
*/
class Resources extends \Lia\Compo {
// files, urls, codes, and sorter all have 'css' and 'js' indices expected.
protected $files = [];
protected $urls = [];
protected $codes = [];
protected $sorter = [];
public function onReady(){
$lia = $this->lia;
$lia->addApi('lia:resource.sort',[$this,'setResourceSorter']);
$lia->addApiMethod('lia:resource.sort', 'setResourceSorter');
$lia->addApi('lia:resource.addfile',[$this,'addResourceFile']);
$lia->addApiMethod('lia:resource.addfile', 'addResourceFile');
$lia->addApi('lia:resource.addurl',[$this,'addResourceUrl']);
$lia->addApiMethod('lia:resource.addurl', 'addResourceUrl');
$lia->addApi('lia:resources.gethtml',[$this,'getHeadHtml']);
$lia->addApiMethod('lia:resources.gethtml', 'getHeadHtml');
}
public function onPackageReady(){
parent::onPackageReady();
if ($this->lia->hasApi('lia:config.default')){
$this->lia->api('lia:config.default', 'lia:resource.recompileJsAfter', 60 * 60 * 24 * 30);
$this->lia->api('lia:config.default', 'lia:resource.recompileCssAfter', 60 * 60 * 24 * 30);
$this->lia->api('lia:config.default', 'lia:resource.useCache', true);
$this->lia->api('lia:config.default', 'lia:resource.forceRecompile', false);
}
}
/**
* Returns routes for compiled resource files & later processes said routes
*/
public function routePatternToRes($route,$response=null){
if ($route===false){
$jsMapFile = $this->lia->api('lia:cache.get', $this->cacheMapName('js'));
$jsMap = $jsMapFile ? json_decode(file_get_contents($jsMapFile),true) : [];
$cssMapFile = $this->lia->api('lia:cache.get', $this->cacheMapName('css'));
$cssMap = $cssMapFile ? json_decode(file_get_contents($cssMapFile),true) : [];
$map = array_merge($jsMap, $cssMap);
$map = array_map(function($v){return '/'.$v;}, $map);
return array_values($map);
}
$useCache = $this->lia->api('lia:config.get', 'lia:resource.useCache');
$response->useTheme = false;
$shortName = substr($route->url(),1);
$ext = pathinfo($shortName,PATHINFO_EXTENSION);
$this->cleanExt($ext);
$cacheFileName = $this->cacheMapName($ext);
$fPath = $this->lia->api('lia:cache.get',$shortName);
$staticFile = null;
if (is_file($fPath)){
$staticFile = new \Lia\Utility\StaticFile($fPath);
} else {
$staticFile = new \Lia\Utility\StaticFile($shortName);
}
$response->addHeaders($staticFile->getContentTypeHeaders());
if ($useCache){
$response->addHeaders($staticFile->getCacheHeaders());
if ($staticFile->userHasFileCached){
$response->sendContent = false;
} else {
$c = file_get_contents($fPath);
$response->content = $c;
}
} else {
$cacheFileName = $this->cacheMapName($ext);
$cacheFPath = $this->lia->api('lia:cache.get',$cacheFileName);
if ($cacheFPath == false||!is_file($cacheFPath)){
//@TODO provide a proper error, both header() & comment
$response->content = '/* file not found */';
return;
}
$json = file_get_contents($fPath);
$fileMap = json_decode($json,true);
foreach ($fileMap as $longName => $storedShortName){
if ($shortName!=$storedShortName)continue;
$files = explode('--',$longName);
foreach ($files as $file){
if (!file_exists($file)){
$respone->content .= "\n/* there is a missing file */\n";
continue;
}
$response->content .= "\n".file_get_contents($file)."\n";
}
}
}
}
// APIs
public function setSorter($ext, $sorter){
// @TODO add debugging here, so one can trace the sorce of a previously set style sorter
$this->cleanExt($ext);
if (($this->sorter[$ext]??null)!==null){
throw new \Liaison\Exception\Base("A '{$ext}' sorter has already been set.");
}
//@TODO maybe check if $sorter is callable
$this->sorter[$ext] = $sorter;
}
//@TODO maybe remove public setSorter()...
public function setResourceSorter($ext, $sorter) {
return $this->setSorter($ext,$sorter);
}
public function addResourceFile($file){
if (!is_file($file)){
throw new \Lia\Exception\Base("Path '{$file}' is not a file");
}
$ext = pathinfo($file,PATHINFO_EXTENSION);
$this->cleanExt($ext);
$file = realpath($file);
$this->files[$ext][$file] = $file;
}
public function addResourceUrl($url){
$path = parse_url($url,PHP_URL_PATH);
$ext = pathinfo($path,PATHINFO_EXTENSION);
$this->cleanExt($ext);
$this->urls[$ext][] = $url;
}
public function getHeadHtml(){
$html = "\n"
.' '.$this->getFileTag('js')."\n"
.' '.$this->getFileTag('css')."\n"
.' '.$this->getUrlTag('js')."\n"
.' '.$this->getUrlTag('css')."\n"
// .' '.$this->getScriptCodeTag()."\n"
// .' '.$this->getStyleCodeTag()."\n"
.' '.str_replace("\n","\n ",$this->lia->api('lia:seo.gethtml'))."\n";
$html = "\n ".trim($html)."\n";
return $html;
}
// Utility functions //why are some of these public??
public function getFileTag($ext){
$this->cleanExt($ext);
$url = $this->getCompiledFilesUrl($ext);
if ($url==false)return '';
switch ($ext){
case 'css':
$html = '<link rel="stylesheet" href="'.$url.'" />';
break;
case 'js':
$html = '<script type="text/javascript" src="'.$url.'"></script>';
break;
default:
$html = '';
}
return $html;
}
public function getUrlTag($ext){
$this->cleanExt($ext);
$html = [];
foreach (($this->urls[$ext]??[]) as $url){
switch ($ext){
case 'css':
$html[] = '<link rel="stylesheet" href="'.$url.'" />';
break;
case 'js':
$html[] = '<script type="text/javascript" src="'.$url.'"></script>';
break;
default:
// $html[] = '';
}
}
$output = "\n ".implode("\n ",$html)."\n";
return $output;
}
protected function cleanExt(&$ext){
$ext = strtolower($ext);
if ($ext!='css'&&$ext!='js')throw new \Lia\Exception\Base("Only js and css are allowed at this time, but '{$ext}' was given.");
}
protected function cacheMapName($ext){
return "lia.resource.{$ext}FileMap";
}
// putting together the resource files
public function getSortedFiles($ext){
// @TODO cache the sorted scripts list
$this->cleanExt($ext);
$files = $this->files[$ext] ?? [];
$sorter = $this->sorter[$ext] ?? null;
if ($sorter!==null){
$files = $sorter($files);
}
return $files;
}
public function concatenateFiles($ext){
$this->cleanExt($ext);
$files = $this->getSortedFiles($ext);
$separator = "\n";
$content = [];
foreach ($files as $file){
$content[] = file_get_contents($file);
}
return implode($separator, $content);
}
public function concatenateFileNames($ext){
$this->cleanExt($ext);
$files = $this->getSortedFiles($ext);
$longName = implode('--',$files);
return $longName;
}
public function compileFilesToCache($ext){
$this->cleanExt($ext);
$lia = $this->lia;
$longName = $this->concatenateFileNames($ext);
if ($longName=='')return false;
$cachedMapName = $this->cacheMapName($ext);
$files = $lia->getCacheFile($cachedMapName);
$files = $lia->api('lia:cache.get',$cachedMapName);
$files = $files==false ? [] : json_decode(file_get_contents($files), true);
$shortName = $files[$longName] ?? 'lia-resource.'.uniqid().'.'.$ext;
$cachedFile = $lia->api('lia:cache.get', $shortName);
if ($cachedFile&&!$lia->api('lia:config.get', "lia:resource.forceRecompile"))return $cachedFile;
$content = $this->concatenateFiles($ext);
$path = $lia->api('lia:cache.set',$shortName, $content, 60*60*24*30);
$files[$longName] = $shortName;
$lia->api('lia:cache.set',$cachedMapName, json_encode($files), 60*60*24*30);
return $path;
}
public function getCompiledFilesUrl($ext){
$this->cleanExt($ext);
$file = $this->compileFilesToCache($ext);
if ($file==false)return false;
$longName = $this->concatenateFileNames($ext);
$files = $this->lia->api('lia:cache.get',$this->cacheMapName($ext));
$files = $files==false ? [] : json_decode(file_get_contents($files), true);
$shortName = $files[$longName];
return '/'.$shortName;
}
}