polyfony-inc/polyfony

View on GitHub
Private/Polyfony/Response/HTML.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace Polyfony\Response;

use Polyfony\{ 
    Format, 
    Response, 
    Config, 
    Locales, 
    Element 
};

class HTML {
    
    // store the list of links (css, favicons...)
    protected static array $_links         = [];
    // store the list of scripts (javascript)
    protected static array $_scripts     = [];
    // store the list of metas tags (title, description, robots...)
    protected static array $_metas         = [];

    // set links for the current HTML page
    public static function setLinks(
        array $links, 
        bool $replace_existing=false
    ) :void {
        // if we want to purge existing links
        !$replace_existing ?: self::$_links = []; 
        // href is the key for storing links
        foreach(
            $links as 
            $href_or_index => $attributes_or_href
        ) {
            // if arguments are provided
            if(is_array($attributes_or_href)) {
                // if we have a stylesheet
                if(
                    // if a rel is set, and it's not a stylesheet
                    !array_key_exists('rel',$attributes_or_href) || 
                    // and it's not a stylesheet
                    $attributes_or_href['rel'] == 'stylesheet'
                ) {
                    // also rewrite the path of assets that used relative shortcuts
                    $attributes_or_href['href']     = self::getPublicAssetPath(
                        $href_or_index, 
                        'Css'
                    );
                    // force the type
                    $attributes_or_href['type']     = 'text/css';
                    // force the rel
                    $attributes_or_href['rel']         = 'stylesheet';
                    // if the media key is implicit
                    array_key_exists(
                        'media', 
                        $attributes_or_href
                    ) ?: $attributes_or_href['media'] = 'all';
                    // push the slightly alternated stylesheet
                    self::$_links[$href_or_index] = $attributes_or_href;
                }
                // otherwize we don't know what we're dealing with
                else {
                    // just set its href
                    $attributes_or_href['href'] = $href_or_index;
                    // push that link and its attributes
                    self::$_links[$href_or_index] = $attributes_or_href;
                }
            }
            // else only an href is provided, we assume it is a stylesheet
            else {
                // push that link alone
                self::$_links[$attributes_or_href] = [
                    // we assume it is a generic all media stylesheet
                    'rel'    =>'stylesheet',
                    'type'    =>'text/css',
                    'media'    =>'all',
                    // and we set its href
                    'href'    =>self::getPublicAssetPath($attributes_or_href, 'Css')
                ];
            }
        }
    }

    // set scripts for the current HTML page
    public static function setScripts(
        array $scripts, 
        bool $replace_existing=false
    ) :void {
        // if we want to purge existing scripts
        !$replace_existing ?: self::$_scripts = [];
        // for each script we have to set
        foreach($scripts as $script) {
            // rewrite its path
            self::$_scripts[] = self::getPublicAssetPath($script, 'Js');
        }
    }


    // set metas for the current HTML page
    public static function setMetas(
        array $metas, 
        bool $replace_existing=false
    ) :void {
        // if we want to purge existing metas
        !$replace_existing ?: self::$_metas = [];
        // name is the key for storing metas
        self::$_metas += $metas;
    }

    // shortcuts to quickly set multiple things
    public static function set(
        array $assets, 
        bool $replace_existing=false
    ) :void {
        // for each batch
        foreach(
            $assets as 
            $category => $scripts_or_links_or_metas
        ) {
            // if the batch is scripts
            if($category == 'scripts') {
                self::setScripts(
                    $scripts_or_links_or_metas, 
                    $replace_existing
                );
            }
            // if the batch is links
            elseif($category == 'links') {
                self::setLinks(
                    $scripts_or_links_or_metas, 
                    $replace_existing
                );
            }
            // if the match is metas
            elseif($category == 'metas') {
                self::setMetas(
                    $scripts_or_links_or_metas, 
                    $replace_existing
                );
            }
        }
    }

    // build an html page
    public static function buildAndGetPage(
        string $content
    ) :string {
        
        // initial content
        $page = 
            '<!doctype html><html lang="'.
            Locales::getLanguage().
            '"><head><title>'.
            Format::htmlSafe(isset(self::$_metas['title']) ? self::$_metas['title'] : '').
            '</title><meta http-equiv="content-type" content="text/html; charset=' . 
            Response::getCharset() . '" />';

        // add the meta tags and the links
        $page .= self::buildMetasTags() . self::buildLinksTags();
        // close the head, add the body, and add the scripts
        $page .= '</head><body>' . $content . self::buildScriptsTags();
        // add the profiler (if enabled)
        $page .= Config::get('profiler', 'enable') ? new \Polyfony\Profiler\HTML : '';
        // close the document and return the assembled html page
        return $page . '</body></html>';

    }

    

    // builds meta code
    private static function buildMetasTags() :string {
        // this is where formatted meta tags go
        $metas = '';
        // for each available meta
        foreach(self::$_metas as $name => $content) {
            // add the formated the meta
            $metas .= 
                '<meta name="'.$name.'" content="'.
                Format::htmlAttributeSafe($content).'" />';
        }
        return $metas;

    }

    // build links code
    private static function buildLinksTags() :string {
        // this is where formatted links tags go
        $links = [];
        // pack and minify
        self::packAndMinifyLinks();
        // for each available link
        foreach(
            self::$_links as 
            $href => $attributes
        ) {
            // sort the attributes (compulse order needs)
            ksort($attributes);
            // build as base stylesheet link and merge its attributes
            $links[] = new Element('link', $attributes);
        }
        return implode('', $links);
    }

    private static function buildScriptsTags() :string {
        // this is where formatted scripts tags go
        $scripts = '';
        // deduplicate scripts
        self::$_scripts = array_unique(self::$_scripts);
        // pack and minify
        self::packAndMinifyScripts();
        // for each available script
        foreach(self::$_scripts as $src) {
            // add the formated the script
            $scripts .= '<script type="text/javascript" src="'.$src.'"></script>';
        }
        return $scripts;
    }

    private static function packAndMinifyScripts() :void {
        // if we are allowed to pack js files
        if(self::isPackingOfTheseAssetsAllowed('js')) {
            // generate a unique name for a pack that contains that list of scripts only
            list(
                $pack_name, 
                $path_path
            ) = self::getPackingNameAndPathFor(self::$_scripts,'Js');
            // if the pack file doesn't exist yet or if we're not allowed to use cache in response
            if(self::doesThisPackNeedRegeneration($path_path)) {
                // create the packing directories if they don't already exist
                self::createPackingDirectoriesFor('Js');
                // the contents of the pack
                $pack_contents = '';
                // for each asset
                foreach(self::$_scripts as $index => $file) {
                    // if that file is not remote we can include it in the pack
                    if(!self::isAssetPathRemote($file)) {
                        // append the contents of that file to the pack
                        $pack_contents .= " \n".file_get_contents(
                            '.'.self::getNameWithoutCacheInvalidationString($file)
                        );
                    }
                }
                // populate the cache file
                file_put_contents(
                    $path_path, 
                    self::getMinifiedPackIfAllowed(
                        $pack_contents,  
                        new \MatthiasMullie\Minify\JS
                    )
                );
            }
            // for each asset
            foreach(self::$_scripts as $index => $file) {
                // if that file is not remote it has already been included in the pack
                if(!self::isAssetPathRemote($file)) {
                    // and we remove the origin from the list of script to include
                    unset(self::$_scripts[$index]);
                }
            }
            // add the pack in addition to already existing scripts
            self::$_scripts[] = "/Assets/Js/Cache/{$pack_name}";
        }
    }

    private static function getStylesheetsLinksSortedByMedia() :array {
        // we'll split the stylesheets links by media attribute
        $stylesheets_by_media = [];
        // iterate over links
        foreach(self::$_links as $index => $attributes) {
            // if the link is a stylesheet and it is local
            if(
                // is it a stylesheet ?
                $attributes['rel'] == 'stylesheet' && 
                // is it a local one ?
                !self::isAssetPathRemote($attributes['href'])
            ) {
                // spool it by its media
                $stylesheets_by_media[$attributes['media']][] = $attributes;
                // and remove it from the list of links to build
                unset(self::$_links[$index]);
            }
        }
        // return both
        return $stylesheets_by_media;
    }

    private static function packAndMinifyLinks() :void {
        // if we are allowed to pack js files
        if(self::isPackingOfTheseAssetsAllowed('css')) {
            // now that we have sorted links by type and medias, we can pack them by medias
            foreach(self::getStylesheetsLinksSortedByMedia() as $media => $list_of_stylesheets) {
                // define a name and path for that pack
                list($pack_name, $pack_path) = self::getPackingNameAndPathFor($list_of_stylesheets, 'Css');
                // if the file does not exist, or if we're not allowed to use cached items
                if(self::doesThisPackNeedRegeneration($pack_path)) {
                    // create the packing directories if they don't already exist
                    self::createPackingDirectoriesFor('Css');
                    // the contents of the pack
                    $pack_contents = '';
                    // foreach stylesheet for this media
                    foreach($list_of_stylesheets as $attributes) {
                        // get the contents of that stylesheet
                        $pack_contents .= file_get_contents(
                            '.'.self::getNameWithoutCacheInvalidationString($attributes['href'])
                        );
                    }
                    // then add to the pack
                    file_put_contents($pack_path, self::getMinifiedPackIfAllowed($pack_contents, new \MatthiasMullie\Minify\CSS));
                }
                // add our pack to the list of links
                self::$_links[] = [
                    'href'    =>"/Assets/Css/Cache/{$pack_name}",
                    'type'    =>'text/css',
                    'media'    =>$media,
                    'rel'    =>'stylesheet'
                ];
                // free up memory
                unset($pack_contents);
            }
        }
    }

    // remove the ?timestamp from a path, if any
    private static function getNameWithoutCacheInvalidationString(string $path) :string {
        // based on the presence of ?
        return strpos($path, '?') !== false ? 
            explode('?', $path)[0] : 
            $path;
    }

    // assets externalness detector
    private static function isAssetPathRemote(string $asset_path) :bool {
        // check is that asset is external to our server, or absolute (suspicously dynamic)
        return 
            substr($asset_path,0,2) == '//' || 
            substr($asset_path,0,4) == 'http';
    }

    private static function isPackingOfTheseAssetsAllowed(
        string $asset_type
    ) :bool {
        // we have to be in prod, and the packing of this type of assets has to be allowed
        return 
            Config::get('response','pack_'.$asset_type) == '1';
    }

    // if this pack needs to be generated again
    private static function doesThisPackNeedRegeneration(
        string $pack_path
    ) :bool {
        return 
            // if the file does not exist
            !file_exists($pack_path) || 
            // or if we're not allowed to use cached items
            !Config::get('response','cache');
    }

    // assets path converter
    private static function getPublicAssetPath(
        string $asset_path, 
        string $asset_type
    ) :string {
        
        // this is a remote ressource
        if(self::isAssetPathRemote($asset_path)) {
            // return it as-is
            return $asset_path;
        }
        // this is a local ressource
        else {
            // build the public path
            $public_path = "/Assets/{$asset_type}/$asset_path";
            // get the modification timestamp of the asset
            $modification = filemtime('../Public'.$public_path);
            // append the modification date to the public path
            $public_path .= '?'.$modification;
            // return the assembled public path
            return $public_path;
        }
    }

    public static function getPackingNameAndPathFor(
        array $assets, 
        string $asset_type
    ) :array {
        // generate a unique name for that list of assets, and the right extension
        $name = \Polyfony\Hashs::get($assets).'.'.strtolower($asset_type);
        // generate a private path to store that file
        $path = "../Private/Storage/Cache/Assets/{$asset_type}/{$name}";
        // return both
        return [$name, $path];
    }

    // minify the packed content if it's allowed, otherwise return is at is
    private static function getMinifiedPackIfAllowed(
        string $pack_contents, 
        object $packer_object=null
    ) :string {
        // if we are allowed to minify
        return Config::get('response','minify') ? 
            ($packer_object)->add($pack_contents)->minify() : 
            $pack_contents;

    }

    // initialize the packing of assets (directory structure)
    public static function createPackingDirectoriesFor(
        string $asset_type
    ) :void {
        // if it doesn't exist create a folder to store packed files
        is_dir("../Private/Storage/Cache/Assets/{$asset_type}/") ?:     
            mkdir("../Private/Storage/Cache/Assets/{$asset_type}/", 0777, true);
        // if the general public assets folder doesn't exist we create it
        is_dir("./Assets/{$asset_type}/") ?:     
            mkdir("./Assets/{$asset_type}/", 0777, true);
        // create a link from the public to the private place where caches are generated
        is_link("./Assets/{$asset_type}/Cache") ?:     
            symlink("../../../Private/Storage/Cache/Assets/{$asset_type}/", "./Assets/{$asset_type}/Cache");
    }

    // push the assets using HTTP/2 
    public static function pushAssets() :void {
        // a list of assets, in a format suited for the Response::push() method
        $assets = [];
        // for each link type asset of the current webpage
        foreach(self::$_links as $link) {
            // if it's CSS, declare it as such
            if($link['type'] == 'text/css') {
                $assets[$link['href']] = 'style';
            }
            // if it's an image, declare it as such
            elseif(stripos($link['type'], 'image') !== false) {
                $assets[$link['href']] = 'image';
            }
        }
        // for each script type asset of the current webpage
        foreach(self::$_scripts as $script) {
            // declare it as a script file
            $assets[$script] = 'script';
        }
        // push all those using HTTP/2
        Response::push($assets);

    }

}

?>