Skip to content

Instantly share code, notes, and snippets.

@isuke01
Created April 16, 2025 07:42
Show Gist options
  • Save isuke01/d3fa4516ec69713af3e1ea1aa7859bbb to your computer and use it in GitHub Desktop.
Save isuke01/d3fa4516ec69713af3e1ea1aa7859bbb to your computer and use it in GitHub Desktop.
WordPress Asset Version Manager A class to manage asset versioning in WordPress by replacing version strings with customizable alternatives like file modification times.
<?php
/**
* WordPress Asset Version Manager
*
* A class to manage asset versioning in WordPress by replacing version strings
* with customizable alternatives like file modification times.
*/
class WP_Asset_Version_Manager {
// Default settings.
private $settings = [
'handle_core' => true,
'core_strategy' => 'file_time',
'plugin_strategy' => 'file_time',
'theme_strategy' => 'keep_original',
'target_plugins' => [],
'target_themes' => [],
'fallback_version' => 'ft',
];
private $content_dir;
private $content_url;
private $content_path;
/**
* Constructor.
*
* @param array $options Configuration options.
*/
public function __construct($options = []) {
// Determine content directory.
$this->content_dir = \defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ABSPATH . 'wp-content';
$this->content_url = \defined( 'WP_CONTENT_URL' ) ? WP_CONTENT_URL : site_url('wp-content');
// Get relative content path for URL matching.
$this->content_path = \str_replace( \site_url(), '', $this->content_url );
// Merge user options with defaults.
$this->settings = \array_merge( $this->settings, $options );
// Hook our method into WordPress.
\add_filter( 'script_loader_src', [$this, 'process_asset_url'], 50, 1 );
\add_filter( 'style_loader_src', [$this, 'process_asset_url'], 50, 1 );
}
/**
* Process asset URL and modify version parameter if needed.
*
* @param string $src The asset URL.
* @return string Modified URL.
*/
public function process_asset_url( $src ) {
// Parse the URL to get components.
$url_parts = \parse_url( $src );
if ( ! isset($url_parts['query'] ) ) {
return $src; // No query parameters to modify.
}
\parse_str( $url_parts['query'], $query_params );
// If no version parameter, nothing to modify.
if ( ! isset( $query_params['ver'] ) ) {
return $src;
}
// Determine asset type
$asset_type = $this->get_asset_type( $src );
// If asset type is unknown, return original URL.
if ( $asset_type === 'unknown' ) {
return $src;
}
// Skip core assets if not enabled
if ( $asset_type === 'core' && ! $this->settings['handle_core'] ) {
return $src;
}
// For plugins, check if we should process this plugin
if ( $asset_type === 'plugin' ) {
$plugin_slug = $this->get_plugin_slug( $src );
if ( empty( $this->settings['target_plugins'] ) || ! in_array( $plugin_slug, $this->settings['target_plugins'] ) ) {
return $src;
}
}
// For themes, check if we should process this theme
else if ( $asset_type === 'theme' ) {
$theme_slug = $this->get_theme_slug( $src );
if ( empty( $this->settings['target_themes'] ) || ! in_array( $theme_slug, $this->settings['target_themes'] ) ) {
return $src;
}
}
// Get the appropriate versioning strategy.
$strategy = $this->settings[$asset_type . '_strategy'];
// Apply the strategy.
switch ( $strategy ) {
case 'file_time':
$version = $this->get_file_time_version( $src );
break;
case 'remove':
unset( $query_params['ver'] );
$version = null;
break;
case 'fixed':
$version = $this->settings['fallback_version'];
break;
case 'keep_original':
default:
return $src; // No change
}
// Set the new version if applicable.
if ( $version !== null ) {
$query_params['ver'] = $version;
}
// Rebuild the query string.
$new_query = \http_build_query( $query_params );
// Rebuild the URL.
$new_src = $url_parts['scheme'] . '://' . $url_parts['host'];
if ( isset($url_parts['port'] ) ) {
$new_src .= ':' . $url_parts['port'];
}
$new_src .= $url_parts['path'];
if ( ! empty( $new_query ) ) {
$new_src .= '?' . $new_query;
}
if ( isset( $url_parts['fragment'] ) ) {
$new_src .= '#' . $url_parts['fragment'];
}
return $new_src;
}
/**
* Determine the type of asset from the URL.
*
* @param string $src The asset URL.
* @return string 'core', 'plugin', 'theme', or 'unknown'.
*/
private function get_asset_type( $src ) {
if ( \strpos( $src, '/wp-includes/' ) !== false || \strpos( $src, '/wp-admin/' ) !== false ) {
return 'core';
}
if ( \strpos( $src, $this->content_path . '/plugins/' ) !== false ) {
return 'plugin';
}
if ( \strpos( $src, $this->content_path . '/themes/' ) !== false ) {
return 'theme';
}
return 'unknown';
}
/**
* Extract plugin slug from URL.
*
* @param string $src The asset URL.
* @return string|null Plugin slug or null if not found.
*/
private function get_plugin_slug( $src ) {
preg_match( '|' . preg_quote($this->content_path ) . '/plugins/([^/]+)|', $src, $matches );
return isset( $matches[1] ) ? $matches[1] : null;
}
/**
* Extract theme slug from URL.
*
* @param string $src The asset URL.
* @return string|null Theme slug or null if not found.
*/
private function get_theme_slug( $src ) {
\preg_match( '|' . \preg_quote( $this->content_path ) . '/themes/([^/]+)|', $src, $matches );
return isset( $matches[1] ) ? $matches[1] : null;
}
/**
* Get file modification time to use as version.
*
* @param string $src The asset URL.
* @return string|int File modification time or fallback.
*/
private function get_file_time_version( $src ) {
// First try to use full URL to path conversion.
$site_url = \site_url();
$file_path = ABSPATH . \str_replace( $site_url, '', \preg_replace('/\?.*/', '', $src ) );
// If file doesn't exist, try alternate path resolution methods.
if ( ! \file_exists( $file_path ) ) {
// Try with content directory mapping.
if ( \strpos( $src, $this->content_url ) !== false ) {
$file_path = \str_replace( $this->content_url, $this->content_dir, \preg_replace( '/\?.*/', '', $src ) );
}
}
// Make sure the file exists and is readable.
if ( \file_exists( $file_path ) && \is_readable( $file_path ) ) {
return \filemtime( $file_path );
}
// Fallback.
return $this->settings['fallback_version'] . \time();
}
/**
* Static convenience method to quickly setup core file versioning.
*
* @param string $strategy Versioning strategy.
* @return WP_Asset_Version_Manager Instance of the manager.
*/
public static function setup_core_versioning( $strategy = 'file_time' ) {
return new self([
'handle_core' => true,
'core_strategy' => $strategy
]);
}
/**
* Static convenience method to setup plugin versioning.
*
* @param array|string $plugin_slugs Plugin slug(s) to target.
* @param string $strategy Versioning strategy.
* @return WP_Asset_Version_Manager Instance of the manager.
*/
public static function setup_plugin_versioning( $plugin_slugs, $strategy = 'plugin_time' ) {
return new self([
'handle_core' => false,
'plugin_strategy' => $strategy,
'target_plugins' => (array) $plugin_slugs
]);
}
/**
* Static convenience method to setup theme versioning.
*
* @param array|string $theme_slugs Theme slug(s) to target.
* @param string $strategy Versioning strategy.
* @return WP_Asset_Version_Manager Instance of the manager.
*/
public static function setup_theme_versioning( $theme_slugs, $strategy = 'file_time' ) {
return new self([
'handle_core' => false,
'theme_strategy' => $strategy,
'target_themes' => (array) $theme_slugs
]);
}
}
// Example usage:
// 1. Basic setup to version core files with file modification time
// $version_manager = WP_Asset_Version_Manager::setup_core_versioning();
// 2. Setup to version a specific plugin's assets based on its main file time
// $version_manager = WP_Asset_Version_Manager::setup_plugin_versioning('woocommerce');
// 3. Custom configuration
// $version_manager = new WP_Asset_Version_Manager([
// 'handle_core' => true,
// 'target_plugins' => ['woocommerce', 'elementor']
// ]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment