Resources.php

<?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;
    }
}