<?php
namespace Lia\Compo;
class Resources extends \Lia\Compo {
/**
* Maybe not so difficult
* - Use config for default site title
* - move SEO code into it's own component
* - Auto-add charset=utf8 & viewport meta tags (with config to disable)
* - Add url prefix for compiled files as a config option
*
* Maybe not so simple
* - Consider using custom routes instead of storing files in a cache-public dir
* - jsFiles & cssFiles arrays have the full file path as their key & value. Add ability to name the paths. This will improve dev experience when sorting a list of scripts/stylesheets
* - Improve sorting
* - add sort preference to `addResourceFile($file)`. A Liaison extension that adds a JS framework would use this to ensure it's framework file is always the first thing.
* - Without this, the sort burden is on the developer who is integrating the Liaison extension.
* - Add a simple function for setting sort preferences, like $lia->showFirst('*autowire.js'); or something
* - Consider other methods of routing that might improve performance...
* - add all seo meta properties that are available...
* - Separate large files from the block of compiled code.
* - If i'm using a, say, 50KB or larger JS file on 10 different pages, but each of those pages has a couple different small JS files for their individual components, it's probably better to cache the 50KB file on its own
* - Minify js & css
*
* @export(TODO.Resources)
*/
// files, urls, codes, and sorter all have 'css' and 'js' indices expected.
protected $files = [];
protected $urls = [];
protected $codes = [];
protected $sorter = [];
protected $seo = [];
public function __construct($package){
parent::__construct($package);
}
public function onStart(){
parent::onStart();
if ($this->lia->hasApi('lia.conf')){
$this->lia->api('lia.conf', 'default', 'lia.maxage.jsCompile', 60 * 60 * 24 * 30);
$this->lia->api('lia.conf', 'default', 'lia.maxage.cssCompile', 60 * 60 * 24 * 30);
$this->lia->api('lia.conf', 'default', 'lia.resource.useCache', true);
$this->lia->api('lia.conf', 'default', 'lia.resource.forceRecompile', false);
}
}
/**
* Handles requests for our compiled resource files
*/
public function routePatternToRes($route){
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.conf', 'get', 'lia.resource.useCache');
if ($useCache){
$shortName = substr($route->url(),1);
$fPath = $this->lia->api('lia.cache','get',$shortName);
$c = file_get_contents($fPath);
// return $c;
echo $c;
} else {
$shortName = substr($route->url(),1);
$ext = pathinfo($shortName,PATHINFO_EXTENSION);
$this->cleanExt($ext);
// echo 'url:'.$route->url()."\n";
// echo 'ext:'.$ext."\n";
$cacheFileName = $this->cacheMapName($ext);
// echo $cacheFileName."\n";
$file = $this->lia->api('lia.cache', 'get', $cacheFileName);
if ($file==false){
//@TODO provide a proper error, both header() & comment
echo '/* file not found */';
return;
}
$json = file_get_contents($file);
$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)){
echo "/* there is a missing file */";
continue;
}
echo file_get_contents($file);
}
}
}
}
// 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 apiSort_lia_resource_setResourceSorter($ext, $sorter) {
return $this->setSorter($ext,$sorter);
}
public function apiAddFile_lia_resource_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 apiAddUrl_lia_resource_addResourceUrl($url){
$path = parse_url($url,PHP_URL_PATH);
$ext = pathinfo($path,PATHINFO_EXTENSION);
$this->cleanExt($ext);
$this->urls[$ext][] = $url;
}
public function apiAdd_lia_seo_seo($params){
if (isset($params['image'])
&&empty($params['image:alt'])){
throw new \Lia\Exception\Base("When setting seo 'image', you MUST set 'image:alt' as well.");
}
foreach ($params as $key=>$value){
$this->seo[$key] = $value;
}
}
public function apiTitle_lia_seo_seoTitle($title){
$this->lia->api('lia.seo', 'add',['title'=>$title]);
}
public function apiDescription_lia_seo_seoDescription($description){
$this->lia->api('lia.seo', 'add',['description'=>$description]);
}
public function apiImage_lia_seo_seoImage($image, $altText){
$this->lia->api('lia.seo', 'add',['image'=>$image, 'image:alt'=>$altText]);
}
public function apiUrl_lia_seo_seoUrl($url){
$this->lia->api('lia.seo', 'add',['url'=>$url]);
}
public function apiSiteName_lia_seo_seoSiteName($siteName){
$this->lia->api('lia.seo', 'add', ['siteName'=>$siteName]);
}
public function apiGetHtml_lia_seo_getSeoHtml(){
// Resources for needed seo data
// https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/getting-started
// https://ogp.me/
// https://www.w3schools.com/tags/tag_meta.asp
// NOT USED https://schema.org
$map = [
'title'=>[
'<title>%s</title>',
'<meta property="og:title" content="%s" />'
],
'description'=>[
'<meta name="description" content="%s" />',
'<meta property="og:description" content="%s" />',
],
'image'=>[
'<meta property="og:image" content="%s" />',
],
'image:alt'=>[
'<meta property="og:image:alt" content="%s" />',
],
'keywords'=>[
'<meta name="keywords" content="%s" />',
],
'url' =>[
'<link rel="canonical" href="%s" />',
'<meta name="og:url" content="%s" />',
],
'siteName'=>[
'<meta name="og:site_name" content="%s" />',
]
];
$html = [];
foreach ($this->seo as $param=>$value){
// $html[] = "$param::$value";
$bits = $map[$param] ?? [];
foreach ($bits as $template){
$html[] = sprintf($template, $value);
}
$html[] = '';
}
$html = implode("\n", $html);
return $html;
}
public function apiGetHtml_lia_resources_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);
//@TODO where do I use lia.resource.useCache? I'm not sure which needs to be set. Maybe both?
if ($cachedFile&&!$lia->api('lia.conf', '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;
}
}