Resources.php

<?php

namespace Lia\Addon;


/**
 * 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
 * @todo rename Resources to Files? & rename some methods?
 */
class Resources extends \Lia\Addon {

    public string $fqn = 'lia:server.resources';

    // files, urls, codes, and sorter all have 'css' and 'js' indices expected.
    public $files = [];
    public $urls = [];
    // public $codes = [];
    public $sorter = [];

    /** an instance of Lia\Addon\Cache
     */
    public $cache;

    /**
     * to minify css & js or not
     * @note not currently implemented
     * @note $_GET['true'] or $_GET['false'] will change this setting
     */
    public $minify = true;

    /**
     * Whether to enable cache or not.
     *
     * @tip disable this during development
     */
    public $useCache = true;

    public function __construct(?\Lia\Package $package=null){
        parent::__construct($package);

        if (isset($_GET['minify'])){
            if ($_GET['minify']=='true')$this->minify = true;
            else if ($_GET['minify']=='false')$this->minify = false;
        }
    }
    public function init_lia(){
        $lia = $this->lia;
        $lia->methods['setResourceSorter'] = [$this,'setSorter'];
        $lia->methods['addResourceFile'] = [$this,'addFile'];
        $lia->methods['addResourceUrl'] = [$this,'addUrl'];
        $lia->methods['getResourceHtml'] = [$this,'getHtml'];
        
        //deprecated alias
        $lia->methods['getHeadHtml'] = [$this,'getHtml'];
    }

    public function onPackageReady(){
        $this->cache = new \Lia\Addon\Cache();
        $this->cache->lia = $this->lia;
        $this->cache->dir = $this->package->dir('cache-resources');

        \Lia\FastFileRouter::file($this->cache->dir, $_SERVER['REQUEST_URI'], '.file');
    }

////////
// Small simple methods
////////

    protected function cleanExt(&$ext){
        $ext = strtolower($ext);
        if ($ext!='css'&&$ext!='js')throw new \Lia\Exception("Only js and css are allowed at this time, but '{$ext}' was given.");
    }
    protected function cacheMapName($ext){
        return "lia.resource.{$ext}FileMap";
    }

/////
// for users
/////
    public function setSorter($ext, $sorter){
        $this->cleanExt($ext);
        $this->sorter[$ext] = $sorter;
    }

    /**
     * Add a css or js file to be added to the final output
     *
     * @param string $file filename
     */
    public function addFile(string $file){
        if (!is_file($file)){
            throw new \Exception("Cannot add file to Resources. Path '{$file}' is not a file");
        }
        $ext = pathinfo($file,PATHINFO_EXTENSION);
        $this->cleanExt($ext);
        $file = realpath($file);
        $this->files[$ext][$file] = $file;
    }
    public function addUrl($url){
        $path = parse_url($url,PHP_URL_PATH);
        $ext = pathinfo($path,PATHINFO_EXTENSION);
        $this->cleanExt($ext);
        $this->urls[$ext][] = $url;
    }

    public function getHtml(){
        $seo_html = isset($this->lia->methods['getSeoHtml']) ?
           '    '.str_replace("\n","\n    ",$this->lia->getSeoHtml())."\n"
           : '';
        $html = "\n"
            .'    '.$this->getFileTag('js')."\n"
            .'    '.$this->getFileTag('css')."\n"
            .'    '.$this->getUrlTag('js')."\n"
            .'    '.$this->getUrlTag('css')."\n"
            // .'    '.$this->getScriptCodeTag()."\n"
            // .'    '.$this->getStyleCodeTag()."\n"
            .$seo_html
            ;
        $html = "\n    ".trim($html)."\n";

        return $html;
    }

//////
// get html
////// 
    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;
    }

    
//////
//  terrible, awful methods that are essential for putting files together
//  these definitely need refactoring
//////

    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);
        
        $longName = $this->concatenateFileNames($ext);
        if ($longName=='')return false;
        $cachedMapName = $this->cacheMapName($ext);
        $files = $this->cache->get_cache_file_content($cachedMapName);
        $files = $files==false ? [] : json_decode($files, true);

        $min = '';
        if ($this->minify)$min = 'min.';
        $shortName = $files[$longName] ?? 'lia-resource.'.uniqid().'.'.$min.$ext;
        $cachedFile = $this->cache->get_cache_file_path($shortName);
        if ($cachedFile&&$this->useCache)return $cachedFile;
        $content = $this->concatenateFiles($ext);

        if ($this->minify){
            $content = $this->minifyFiles($content,$ext);
        }

        $path = $this->cache->cache_file($shortName, $content, 60*60*24*30);
        $files[$longName] = $shortName;
        $this->cache->cache_file($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->cache->get_cache_file_content($this->cacheMapName($ext));
        $files = $files==false ? [] : json_decode($files, true);
        $shortName = $files[$longName] ?? null;

        return '/'.$shortName;
    }

    /**
     * @warning There is no minification right now. This function is a placeholder
     */
    public function minifyFiles(string $fileContent, string $ext){
        // css + js minifier: https://github.com/matthiasmullie/minify
        // css + js minifier: https://github.com/ceesvanegmond/minify
        // css + js minifier (codeigniter): https://github.com/slav123/CodeIgniter-minify
        if ($ext=='js')return $fileContent;
        else if ($ext=='css'){
            return $fileContent;
            //css minification options:
            // https://github.com/Cerdic/CSSTidy


            //
            // if (class_exists('csstidy',true)){
            //     $csstidy = new \csstidy();
            //
            //     // Set some options :
            //     // $csstidy->set_cfg('optimise_shorthands', 2);
            //     $csstidy->set_cfg('template', 'high');
            //
            //     // Parse the CSS
            //     $csstidy->parse($fileContent);
            //
            //     // Get back the optimized CSS Code
            //     $css_code_opt = $csstidy->print->plain();
            //     return $css_code_opt;
            // }

        }
    }
}