x3p0-dev / x3p0-asset
A small, modern asset-resolution library for WordPress plugins and themes.
Requires
- php: >=8.1
Requires (Dev)
This package is auto-updated.
Last update: 2026-07-09 18:27:58 UTC
README
A small, modern asset-resolution library for WordPress plugins and themes. It
turns a project-relative path (public/css/screen.css) into a value object that
knows its public URL, absolute path, and — when the file was built with
@wordpress/scripts
— its dependencies and cache-busting version, read automatically from the
generated .asset.php file.
Features
- One value type:
Assetdescribes any bundled file — script, style, image, font, audio — with its URL and filesystem path. - Automatic build metadata: reads the
.asset.phpfile emitted by@wordpress/scripts, sodependencies()andversion()are filled in for you. - Sensible fallback: files with no
.asset.phpfall back to the file's modification time for cache busting. - Location resolvers: pick where paths resolve — the active theme, the parent theme, or a plugin — behind one abstract
AssetResolver. - WordPress-friendly, core-free values: only the resolvers call WordPress functions; the
Assetvalue itself stays free of WordPress dependencies. - Type-Safe: full PHP 8.1+ type declarations for better IDE support.
Requirements
- PHP 8.1 or higher
- WordPress (recommended latest version)
- Composer
Installation
Install via Composer:
composer require x3p0-dev/x3p0-asset
Important: If you're releasing this as part of a theme or plugin bundle, please vendor prefix your installation to avoid conflicts with other plugins/themes. See Vendor Prefixing below.
Quick Start
1. Pick a resolver
A resolver decides where relative paths are resolved from. Bundle a single resolver of your chosen direction and ask it for every asset:
use X3P0\Asset\ThemeAssetResolver; $assetResolver = new ThemeAssetResolver();
2. Get an asset
$style = $assetResolver->asset('public/css/screen.css');
3. Use it
The asset carries everything the WordPress enqueue functions need:
wp_enqueue_style( 'my-theme-screen', $style->fileUrl(), $style->dependencies(), $style->version() ); // Register the path so WordPress can inline the stylesheet when it's small. wp_style_add_data('my-theme-screen', 'path', $style->filePath());
For a script whose build emitted public/js/editor.asset.php, the dependencies
and version come straight from that file:
$script = $assetResolver->asset('public/js/editor.js'); wp_enqueue_script( 'my-theme-editor', $script->fileUrl(), $script->dependencies(), // e.g. ['wp-blocks', 'wp-element'] $script->version(), // e.g. the build hash true );
Core Concepts
The Asset value
Asset extends SplFileInfo, so every filesystem helper you already know
(getSize(), getMTime(), getExtension(), …) is available alongside these:
| Method | Returns |
|---|---|
fileUrl() |
Public URL to the file |
filePath() |
Absolute filesystem path to the file |
dependencies() |
Registered dependencies, or [] when there's no .asset.php |
version() |
Build hash from the .asset.php, otherwise the file's modification time |
hasDataFile() |
Whether the .asset.php data file exists |
An Asset is location-agnostic: it's constructed with an already-resolved
absolute path and public URL, so it holds no knowledge of themes or plugins. You
normally don't construct it directly — a resolver mints it for you.
The .asset.php data file
When you build scripts and styles with @wordpress/scripts, the
dependency-extraction plugin
emits a companion asset file next to each entry point — editor.js gets
editor.asset.php — returning an array of dependencies and a version.
Asset finds that file automatically (its own name with the extension swapped
for .asset.php) and uses it:
dependencies()returns the array'sdependencies, or[]if the file is absent.version()returns the array'sversion, or the built file's modification time if the file is absent.hasDataFile()reports whether the file exists — useful for skipping registration when a build output is missing.
The lookup is memoized, so the file is checked and included at most once per
Asset.
Resolvers
An AssetResolver resolves relative paths against a base location and mints
Asset objects that live there. The base — the one thing that differs between a
plugin and a theme — is supplied by the concrete resolvers:
| Resolver | Resolves against | Backing functions |
|---|---|---|
ThemeAssetResolver |
the active theme (child-overridable) | get_theme_file_path() / get_theme_file_uri() |
ParentThemeAssetResolver |
the parent theme (always ships from the parent) | get_parent_theme_file_path() / get_parent_theme_file_uri() |
PluginAssetResolver |
a plugin directory | plugin_dir_path() / plugins_url() |
Each exposes:
$assetResolver->asset('public/js/app.js'); // Asset from a relative path $assetResolver->fromFile($splFileInfo); // Asset from a discovered file $assetResolver->path('public/js/app.js'); // absolute filesystem path (string) $assetResolver->url('public/js/app.js'); // public URL (string) $assetResolver->relativize($absolutePath); // absolute path -> base-relative path
PluginAssetResolver takes the plugin's main file so it can anchor both the
path and the URL:
use X3P0\Asset\PluginAssetResolver; // Typically from the plugin's bootstrap file. $assetResolver = new PluginAssetResolver(__FILE__);
Choosing a resolver
ThemeAssetResolverlets a child theme override a bundled file by shipping its own copy at the same relative path. Good for assets a child theme should be able to replace (images, fonts, editor styles).ParentThemeAssetResolveralways loads from the theme that ships the file, even when a child theme is active. Good for built scripts and styles that belong to the parent.PluginAssetResolverresolves against a plugin directory.
A project generally binds one resolver and uses it everywhere.
Discovering assets
fromFile() mints an Asset from an already-discovered SplFileInfo, deriving
its base-relative path via relativize(). This pairs well with directory
iteration — for example, collecting every built block stylesheet in a folder:
foreach ($cssFiles as $file) { $asset = $assetResolver->fromFile($file); if ($asset->hasDataFile()) { // register/enqueue $asset->fileUrl(), $asset->dependencies(), ... } }
Errors
Every exception the package throws implements the AssetException marker
interface (which extends Throwable), so you can catch anything originating
here in a single block. Each concrete exception also extends the most fitting
SPL class, so code that only cares about the SPL type keeps working too.
| Exception | Extends | Thrown when |
|---|---|---|
PathOutsideBaseException |
InvalidArgumentException |
relativize() / fromFile() receive a path that isn't within the resolver's base |
InvalidAssetDataException |
UnexpectedValueException |
an .asset.php file exists but does not return an array (a malformed build) |
use X3P0\Asset\AssetException; try { $asset = $assetResolver->fromFile($file); $deps = $asset->dependencies(); } catch (AssetException $e) { // Any failure from this package: bad path, malformed build metadata, etc. }
Both are programmer/build errors rather than routine conditions — in correct usage neither fires — so catching them is optional. The exceptions carry no WordPress dependency, so they behave the same whether or not WordPress is loaded.
Dependency Injection
The library has no container of its own, but it's designed to be bound in one.
Bind the abstract AssetResolver to the concrete resolver your project uses,
then type-hint AssetResolver wherever you need assets:
use X3P0\Asset\AssetResolver; use X3P0\Asset\ThemeAssetResolver; // Wherever you register bindings: $container->singleton(AssetResolver::class, ThemeAssetResolver::class);
use X3P0\Asset\AssetResolver; final class FrontendAssets { public function __construct(private readonly AssetResolver $assetResolver) {} public function enqueue(): void { $style = $this->assetResolver->asset('public/css/screen.css'); wp_enqueue_style( 'my-theme-screen', $style->fileUrl(), $style->dependencies(), $style->version() ); } }
Swapping the whole project between the active theme and the parent theme is then a one-line change to the binding.
Vendor Prefixing
Because WordPress loads every active plugin and theme into the same PHP process, two of them shipping the same un-prefixed library will collide. If you distribute your plugin or theme, prefix this package's namespace so your copy is isolated from everyone else's.
The X3P0 projects do this at build time with
x3p0-dev/x3p0-prelude, which copies the
dependency into your project and rewrites its namespace under your own — for
example, X3P0\Asset becomes Acme\MyPlugin\Asset. A general-purpose
alternative is PHP-Scoper.
Prefix at release time, not during development, and point your autoloader at the prefixed copy.
License
X3P0: Asset is licensed under the GPL-2.0-or-later license.
Credits
Created and maintained by Justin Tadlock under the X3P0 umbrella.