Same name in other branches
- 5.0.x src/Asset/AssetOptimizer.php \Drupal\advagg\Asset\AssetOptimizer
- 6.0.x src/Asset/AssetOptimizer.php \Drupal\advagg\Asset\AssetOptimizer
- 8.x-4.x src/Asset/AssetOptimizer.php \Drupal\advagg\Asset\AssetOptimizer
Defines the base AdvAgg optimizer.
Hierarchy
- class \Drupal\advagg\Asset\AssetOptimizer
Expanded class hierarchy of AssetOptimizer
4 files declare their use of AssetOptimizer
- advagg.install in ./
advagg.install - Handles Advanced Aggregation installation and upgrade tasks.
- advagg.module in ./
advagg.module - Advanced CSS/JS aggregation module.
- advagg_mod.module in advagg_mod/
advagg_mod.module - Advanced aggregation modifier module.
- SettingsForm.php in src/
Form/ SettingsForm.php
File
-
src/
Asset/ AssetOptimizer.php, line 13
Namespace
Drupal\advagg\AssetView source
abstract class AssetOptimizer {
/**
* Checks for and if found fixes incorrectly set asset types.
*
* @param array $asset
* A core single asset definition array.
*/
protected abstract function fixType(array &$asset);
/**
* Asset type (css or js).
*
* @var string
*/
protected $extension;
/**
* A config object for the advagg configuration.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* Event Dispatcher service.
*
* @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
*/
protected $eventDispatcher;
/**
* The AdvAgg cache.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* Config level of caching of assets.
*
* @var int
*/
protected $cacheLevel;
/**
* The cache time.
*
* @var int
*/
protected $cacheTime;
/**
* Config to control fixing the asset type (file, external).
*
* @var bool
*/
protected $fixType;
/**
* Whether or not to gzip assets.
*
* @var bool
*/
protected $gZip;
/**
* Whether or not to brotli compress assets.
*
* @var bool
*/
protected $brotli;
/**
* Array of domains to prefetch. Copied to $GLOBALS for later use.
*
* @var array
*/
protected $dnsPrefetch;
/**
* Constructs the Optimizer object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* A config factory for retrieving required config objects.
* @param \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher $event_dispatcher
* The event dispatcher.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The AdvAgg cache.
*/
public function __construct(ConfigFactoryInterface $config_factory, ContainerAwareEventDispatcher $event_dispatcher, CacheBackendInterface $cache) {
$this->config = $config_factory->get('advagg.settings');
$this->eventDispatcher = $event_dispatcher;
$this->cache = $cache;
$this->dnsPrefetch = [];
$this->cacheLevel = $this->config
->get('cache_level');
$this->fixType = $this->config
->get("{$this->extension}.fix_type");
if ($this->fixType) {
$this->basePath = substr($GLOBALS['base_root'] . $GLOBALS['base_path'], strpos($GLOBALS['base_root'] . $GLOBALS['base_path'], '//') + 2);
$this->basePathLen = strlen($this->basePath);
}
$this->gZip = $this->shouldGZip();
$this->brotli = $this->shouldBrotli();
}
/**
* Process a core asset array.
*
* @param array $assets
* The core asset array (css or js) to process.
*/
public function processAssetArray(array &$assets) {
$protocol_relative = $this->config
->get('path.convert.absolute_to_protocol_relative');
$force_https = $this->config
->get('path.convert.force_https');
foreach ($assets as &$asset) {
if (!is_string($asset['data'])) {
continue;
}
// Fix type if it was incorrectly set.
if ($this->fixType) {
$this->fixType($asset);
}
if ($asset['type'] === 'file' && $asset['preprocess']) {
if (!is_readable($asset['data'])) {
continue;
}
$this->scanFile($asset);
}
elseif ($asset['type'] === 'external') {
if ($force_https) {
$asset['data'] = $this->convertPathForceHttps($asset['data']);
}
elseif ($protocol_relative) {
$asset['data'] = $this->convertPathProtocolRelative($asset['data']);
}
$scheme = parse_url($asset['data'], PHP_URL_SCHEME);
$host = parse_url($asset['data'], PHP_URL_HOST);
$asset_url = isset($scheme) ? "{$scheme}://{$host}" : "//{$host}";
$this->dnsPrefetch[] = $asset_url;
}
}
if (!isset($GLOBALS['_advagg_prefetch'])) {
$GLOBALS['_advagg_prefetch'] = [];
}
$GLOBALS['_advagg_prefetch'] += $this->dnsPrefetch;
}
/**
* Given a filename calculate various hashes, gather meta data then optimize.
*
* If any file optimizations are applied, updates the asset array.
* Also if enabled preemptively creates compressed versions.
*
* @param array $asset
* A core asset array.
*/
protected function scanFile(array &$asset) {
// Clear PHP's internal file status cache.
clearstatcache(TRUE, $asset['data']);
$cid = Crypt::hashBase64($asset['data'] . $this->config
->get('global_counter'));
$cached = $this->cache
->get($cid);
if ($cached && file_exists($cached->data['file'])) {
if ($this->config
->get('css.combine_media') && isset($asset['media']) && $asset['media'] !== 'all') {
$asset['media'] = 'all';
}
$asset['size'] = $cached->data['filesize'];
if ($this->cacheLevel === 3) {
$asset['data'] = $cached->data['file'];
$this->dnsPrefetch += $cached->data['prefetch'];
return;
}
$data = [
'filesize' => (int) @filesize($asset['data']),
'mtime' => @filemtime($asset['data']),
];
if ($this->cacheLevel === 2) {
if ($cached->data['mtime'] === $data['mtime']) {
$asset['data'] = $cached->data['file'];
$this->dnsPrefetch += $cached->data['prefetch'];
return;
}
}
$data['contents'] = @file_get_contents($asset['data']);
$data['hash'] = Crypt::hashBase64($data['contents']);
if ($this->cacheLevel === 1) {
if ($cached->data['hash'] === $data['hash']) {
$asset['data'] = $cached->data['file'];
$this->dnsPrefetch += $cached->data['prefetch'];
return;
}
}
}
if (empty($data)) {
$data = [
'filesize' => (int) @filesize($asset['data']),
'mtime' => @filemtime($asset['data']),
'contents' => @file_get_contents($asset['data']),
];
$data['hash'] = Crypt::hashBase64($data['contents']);
}
$data['cid'] = $cid;
$asset['size'] = $data['filesize'];
if ($data['file'] = $this->optimizeFile($asset, $data)) {
$asset['contents'] = $data['contents'];
$data['prefetch'] = $this->addDnsPrefetch($asset);
$this->dnsPrefetch += $data['prefetch'];
$data['original'] = $asset['data'];
unset($data['contents']);
unset($data['cid']);
$this->cache
->set($cid, $data, $this->getCacheTime(), [
'advagg',
]);
$asset['data'] = $data['file'];
}
}
/**
* The filename for the CSS or JS optimized file is the cid.
*
* The CID is generated from the hashed original filename.
*
* @param string $data
* The content to output.
* @param string $cid
* The unique segment of the filename.
*
* @return bool|string
* FALSE or the saved filename.
*/
protected function writeFile($data, $cid) {
// Prefix filename to prevent blocking by firewalls which reject files
// starting with "ad*".
// Create the css/ or js/ path within the files folder.
$path = 'public://' . $this->extension . '/optimized';
$version = Crypt::hashBase64($data);
$uri = "{$path}/{$this->extension}_{$cid}.{$version}.{$this->extension}";
// Create the CSS or JS file.
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
if (!file_exists($uri)) {
if (!file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) {
return FALSE;
}
}
// If CSS/JS gzip compression is enabled and the zlib extension is available
// then create a gzipped version of this file. This file is served
// conditionally to browsers that accept gzip using .htaccess rules.
if ($this->gZip && !file_exists($uri . '.gz')) {
file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE);
}
// If brotli compression is enabled and available, create br compressed
// files and serve conditionally via .htaccess rules.
if ($this->brotli && !file_exists($uri . '.br')) {
file_unmanaged_save_data(brotli_compress($data, 11, BROTLI_TEXT), $uri . '.br', FILE_EXISTS_REPLACE);
}
return $uri;
}
/**
* Determine if settings and available PHP modules allow GZipping assets.
*
* @return bool
* True if asset type can/should be gzipped.
*/
protected function shouldGZip() {
if (extension_loaded('zlib') && \Drupal::config('system.performance')->get($this->extension . '.gzip')) {
return TRUE;
}
return FALSE;
}
/**
* Determine if settings and available PHP modules allow brotli-ing assets.
*
* @return bool
* True if asset type can/should be brotli-ed.
*/
protected function shouldBrotli() {
if (function_exists('brotli_compress') && $this->config
->get($this->extension . '.brotli')) {
return TRUE;
}
return FALSE;
}
/**
* Get how long to cache an asset. Varies on cache level setting.
*
* @return int
* The seconds to cache the data for.
*/
protected function getCacheTime() {
if ($this->cacheTime) {
return $this->cacheTime;
}
$this->cacheTime = (int) microtime(TRUE);
// 1 Day.
if ($this->cacheLevel === 1) {
$this->cacheTime += 86400;
}
elseif ($this->cacheLevel === 2) {
$this->cacheTime += 604800;
}
elseif ($this->cacheLevel === 3) {
$this->cacheTime += 2419200;
}
return $this->cacheTime;
}
/**
* Perform any in-place optimization & pass to event for further optimization.
*
* @param array $asset
* Core single asset definition array.
* @param array $data
* An array of extra file information (hashes, modification time etc).
*
* @return bool|string
* False if contents unchanged or the new file path if optimized.
*/
protected abstract function optimizeFile(array &$asset, array $data);
/**
* Extract any domains to prefetch DNS.
*
* @param array $asset
* A core asset definition array.
*
* @return array
* An array of domains to prefetch.
*/
protected abstract function addDnsPrefetch(array $asset);
/**
* Converts absolute paths to be protocol relative paths.
*
* @param string $path
* Path to check.
*
* @return string
* The converted path or the original path if already protocol relative.
*/
protected function convertPathProtocolRelative($path) {
if (strpos($path, 'https://') === 0) {
$path = substr($path, 6);
}
elseif (strpos($path, 'http://') === 0) {
$path = substr($path, 5);
}
return $path;
}
/**
* Convert http:// to https://.
*
* @param string $path
* Path to check.
*
* @return string
* The modified path or the original if already https or relative.
*/
protected function convertPathForceHttps($path) {
if (strpos($path, 'http://') === 0) {
$path = 'https://' . substr($path, 7);
}
return $path;
}
/**
* Stable sort for CSS and JS items.
*
* Preserves the order of items with equal sort criteria.
*
* The function will sort by:
* - $item['group'], integer, ascending
* - $item['weight'], integer, ascending
*
* @param array &$assets
* Array of JS or CSS items, as in hook_alter_js() and hook_alter_css().
* The array keys can be integers or strings. The items are arrays.
*
* @see hook_alter_js()
* @see hook_alter_css()
*/
public static function sortStable(array &$assets) {
$nested = [];
foreach ($assets as $key => $item) {
// Weight cast to string to preserve float.
$weight = (string) $item['weight'];
$nested[$item['group']][$weight][$key] = $item;
}
// First order by group, so that, for example, all items in the CSS_SYSTEM
// group appear before items in the CSS_DEFAULT group, which appear before
// all items in the CSS_THEME group. Modules may create additional groups by
// defining their own constants.
$sorted = [];
// Sort group; then iterate over it.
ksort($nested);
foreach ($nested as &$group_items) {
// Order by weight and iterate over it.
ksort($group_items);
foreach ($group_items as &$weight_items) {
foreach ($weight_items as $key => &$item) {
$sorted[$key] = $item;
}
unset($item);
}
unset($weight_items);
}
unset($group_items);
$assets = $sorted;
}
/**
* Generate an htaccess file in the optimized asset dirs to improve serving.
*
* @param string $extension
* The file type to use - either CSS or JS.
* @param bool $regenerate
* Whether to regenerate if the file already exists.
*/
public static function generateHtaccess($extension, $regenerate = FALSE) {
$path = "public://{$extension}/optimized";
$file = $path . '/.htaccess';
if (!$regenerate && file_exists($file)) {
return;
}
/** @var \Drupal\Core\Config\Config $config */
$config = \Drupal::config('advagg.settings');
if ($extension === 'js') {
$type = 'application/javascript';
}
else {
$type = 'text/css';
}
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
$immutable = $config->get('immutable') ? ', immutable' : '';
$options = '';
if ($config->get('symlinks')) {
$options = 'Options +FollowSymlinks';
}
elseif ($config->get('symlinksifownermatch')) {
$options = 'Options +SymLinksIfOwnerMatch';
}
$htaccess = <<<EOT
{<span class="php-variable">$options</span>}
<IfModule mod_rewrite.c>
RewriteEngine on
<IfModule mod_headers.c>
# Serve brotli compressed {<span class="php-variable">$extension</span>} files if they exist and the client accepts br.
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond %{REQUEST_FILENAME}\\.br -s
RewriteRule ^(.*)\\.{<span class="php-variable">$extension</span>} \$1\\.{<span class="php-variable">$extension</span>}\\.br [QSA]
RewriteRule \\.{<span class="php-variable">$extension</span>}\\.br\$ - [T={<span class="php-variable">$type</span>},E=no-gzip:1]
<FilesMatch "\\.{<span class="php-variable">$extension</span>}\\.br\$">
# Serve correct encoding type.
Header set Content-Encoding br
# Force proxies to cache br/gzip/non-gzipped assets separately.
Header append Vary Accept-Encoding
</FilesMatch>
# Serve gzip compressed {<span class="php-variable">$extension</span>} files if they exist and the client accepts gzip.
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond %{REQUEST_FILENAME}\\.gz -s
RewriteRule ^(.*)\\.{<span class="php-variable">$extension</span>} \$1\\.{<span class="php-variable">$extension</span>}\\.gz [QSA]
RewriteRule \\.{<span class="php-variable">$extension</span>}\\.gz\$ - [T=application/javascript,E=no-gzip:1]
<FilesMatch "\\.{<span class="php-variable">$extension</span>}\\.gz\$">
# Serve correct encoding type.
Header set Content-Encoding gzip
# Force proxies to cache br/gzip/non-gzipped assets separately.
Header append Vary Accept-Encoding
</FilesMatch>
</IfModule>
</IfModule>
<FilesMatch "{<span class="php-variable">$extension</span>}(\\.gz|\\.br)?">
# No mod_headers. Apache module headers is not enabled.
<IfModule !mod_headers.c>
# No mod_expires. Apache module expires is not enabled.
<IfModule !mod_expires.c>
# Use ETags.
FileETag MTime Size
</IfModule>
</IfModule>
# Use Expires Directive if apache module expires is enabled.
<IfModule mod_expires.c>
# Do not use ETags.
FileETag None
# Enable expirations.
ExpiresActive On
# Cache all aggregated {<span class="php-variable">$extension</span>} files for 52 weeks after access (A).
ExpiresDefault A31449600
</IfModule>
# Use Headers Directive if apache module headers is enabled.
<IfModule mod_headers.c>
# Do not use etags for cache validation.
Header unset ETag
# Serve correct content type.
Header set Content-Type {<span class="php-variable">$type</span>}
<IfModule !mod_expires.c>
# Set a far future Cache-Control header to 52 weeks.
Header set Cache-Control "max-age=31449600, no-transform, public{<span class="php-variable">$immutable</span>}"
</IfModule>
<IfModule mod_expires.c>
Header append Cache-Control "no-transform, public{<span class="php-variable">$immutable</span>}"
</IfModule>
</IfModule>
ForceType {<span class="php-variable">$type</span>}
</FilesMatch>
EOT;
file_unmanaged_save_data($htaccess, $file, FILE_EXISTS_REPLACE);
}
}
Members
Title Sort descending | Modifiers | Object type | Summary | Overrides |
---|---|---|---|---|
AssetOptimizer::$brotli | protected | property | Whether or not to brotli compress assets. | |
AssetOptimizer::$cache | protected | property | The AdvAgg cache. | |
AssetOptimizer::$cacheLevel | protected | property | Config level of caching of assets. | |
AssetOptimizer::$cacheTime | protected | property | The cache time. | |
AssetOptimizer::$config | protected | property | A config object for the advagg configuration. | |
AssetOptimizer::$dnsPrefetch | protected | property | Array of domains to prefetch. Copied to $GLOBALS for later use. | |
AssetOptimizer::$eventDispatcher | protected | property | Event Dispatcher service. | |
AssetOptimizer::$extension | protected | property | Asset type (css or js). | |
AssetOptimizer::$fixType | protected | property | Config to control fixing the asset type (file, external). | 2 |
AssetOptimizer::$gZip | protected | property | Whether or not to gzip assets. | |
AssetOptimizer::addDnsPrefetch | abstract protected | function | Extract any domains to prefetch DNS. | 2 |
AssetOptimizer::convertPathForceHttps | protected | function | Convert http:// to https://. | |
AssetOptimizer::convertPathProtocolRelative | protected | function | Converts absolute paths to be protocol relative paths. | |
AssetOptimizer::fixType | abstract protected | function | Checks for and if found fixes incorrectly set asset types. | 2 |
AssetOptimizer::generateHtaccess | public static | function | Generate an htaccess file in the optimized asset dirs to improve serving. | |
AssetOptimizer::getCacheTime | protected | function | Get how long to cache an asset. Varies on cache level setting. | |
AssetOptimizer::optimizeFile | abstract protected | function | Perform any in-place optimization & pass to event for further optimization. | 2 |
AssetOptimizer::processAssetArray | public | function | Process a core asset array. | |
AssetOptimizer::scanFile | protected | function | Given a filename calculate various hashes, gather meta data then optimize. | |
AssetOptimizer::shouldBrotli | protected | function | Determine if settings and available PHP modules allow brotli-ing assets. | |
AssetOptimizer::shouldGZip | protected | function | Determine if settings and available PHP modules allow GZipping assets. | |
AssetOptimizer::sortStable | public static | function | Stable sort for CSS and JS items. | |
AssetOptimizer::writeFile | protected | function | The filename for the CSS or JS optimized file is the cid. | |
AssetOptimizer::__construct | public | function | Constructs the Optimizer object. | 2 |