'style', 'script' => 'script', 'font' => 'font', 'document' => 'document', 'image' => 'image', 'url' => '' ]; /** * default link "type" attributes */ const ATTR_TYPE = [ 'font' => 'font/woff2' ]; /** * default additional attributes by $group */ const ATTR_ADD = [ 'font' => ['crossorigin' => 'anonymous'] ]; /** * BASE path * @var string */ private $basePath = ''; /** * absolute file path -> use setOption() for update * @var array */ private $filePath = [ 'style' => '', 'script' => '', 'font' => '', 'document' => '', 'image' => '', 'favicon' => '', 'url' => '' ]; /** * default file extensions by $group * -> used if no fileExtension found in $file * @var array */ private $fileExt = [ 'style' => 'css', 'script' => 'js', 'document' => 'html', 'font' => 'woff2' ]; /** * output type * -> 'inline' -> render inline HTML tags * -> 'header' -> send "Link" HTTP Header with request * @see buildLinks() * @see buildHeader() * @var string */ private $output = 'inline'; /** * resource file cache * @var array */ private $resources = []; /** * set or extend option * @param string $option * @param $value * @param bool $extend */ public function setOption(string $option, $value, bool $extend = false){ $this->$option = ($extend && is_array($value) && is_array($this->$option)) ? array_merge($this->$option, $value) : $value; } /** * get option * @param string $option * @return mixed|null */ public function getOption(string $option){ return isset($this->$option) ? $this->$option : null; } /** * register new resource $file * @param string $group * @param string $file * @param string $rel */ public function register(string $group, string $file, string $rel = self::ATTR_REL){ $this->resources[$group][$file] = ['options' => ['rel' => $rel]]; } /** * get resource path/file.ext * @param string $group * @param string $file * @return string */ public function getLink(string $group, string $file) : string { // $group 'url' expect full qualified URLs $link = ($group == 'url' ? '' : $this->getPath($group) . '/') . $file; // add extension if not already part of the file // -> allows switching between extensions (e.g. .jpg, .png) for the same image $link .= empty(pathinfo($file, PATHINFO_EXTENSION)) ? '.' . $this->getFileExtension($group) : ''; return $link; } /** * get resource path * @param string $group * @return string */ public function getPath(string $group) : string { return rtrim($this->basePath, '/\\') . $this->filePath[$group]; } /** * build inline HTML tags for resources * @return string */ public function buildLinks(){ $this->build(); $links = []; foreach($this->resources as $group => $resources){ foreach($resources as $file => $conf){ $resourceHeader = ' $value){ $resourceHeader .= ' ' . $attr . '="' . $value . '"'; // insert href attr after rel attr -> better readability if($attr == 'rel'){ $resourceHeader .= ' href="' . $conf['link'] . '"'; } } $links[] = $resourceHeader . '>'; } } return "\n\t" . implode("\n\t", $links); } /** * build HTTP header for resource preload * -> all registered resources combined in a single header * @link https://www.nginx.com/blog/nginx-1-13-9-http2-server-push/#automatic-push * @return string */ public function buildHeader() : string { $this->build(); $headers = []; foreach($this->resources as $group => $resources){ foreach($resources as $file => $conf){ $resourceHeader = '<' . $conf['link'] . '>'; foreach($conf['options'] as $attr => $value){ $resourceHeader .= '; ' . $attr . '="' . $value . '"'; } $headers[] = $resourceHeader; } } return 'Link: ' . implode(', ', $headers); } /** * build resource data * -> add missing attributes to resources */ protected function build(){ foreach($this->resources as $group => &$resources){ foreach($resources as $file => &$conf){ if(empty($conf['link'])){ $conf['link'] = $this->getLink($group, $file); } if( empty($conf['options']['rel']) ){ $conf['options']['rel'] = self::ATTR_REL; } if( empty($conf['options']['as']) && !empty($attrAs = $this->getLinkAttrAs($group)) ){ $conf['options']['as'] = $attrAs; } if( empty($conf['options']['type']) && !empty($attrType = $this->getLinkAttrType($group)) ){ $conf['options']['type'] = $attrType; } if( !empty($additionalAttr = $this->getAdditionalAttrs($group)) ){ $conf['options'] = $conf['options'] + $additionalAttr; } } } unset($resources); // unset ref } /** * get 'as' attribute (potential destination) by resource $group * @link https://w3c.github.io/preload/#as-attribute * @param string $group * @return string */ protected function getLinkAttrAs(string $group) : string { return isset(self::ATTR_AS[$group]) ? self::ATTR_AS[$group] : ''; } /** * get 'type' attribute by resource $group * @link https://w3c.github.io/preload/#early-fetch-of-critical-resources * @param string $group * @return string */ protected function getLinkAttrType(string $group) : string { return isset(self::ATTR_TYPE[$group]) ? self::ATTR_TYPE[$group] : ''; } /** * get additional attributes by $group * -> e.g. or fonts * @param string $group * @return array */ protected function getAdditionalAttrs(string $group) : array { return isset(self::ATTR_ADD[$group]) ? self::ATTR_ADD[$group] : []; } /** * get file extension by $group * -> e.g. or fonts * @param string $group * @return string */ protected function getFileExtension(string $group) : string { return isset($this->fileExt[$group]) ? $this->fileExt[$group] : ''; } }