themehybrid/hybrid-assets

Assets helper package for the Hybrid Core framework.

Maintainers

Package info

github.com/themehybrid/hybrid-assets

pkg:composer/themehybrid/hybrid-assets

Transparency log

Statistics

Installs: 34

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

1.0.0-alpha.5 2026-08-01 09:18 UTC

This package is not auto-updated.

Last update: 2026-08-04 06:12:22 UTC


README

Asset (CSS/JS etc.) resolution for Hybrid Core.

Hybrid Assets gives themes and plugins a single, consistent API for resolving asset URLs, filesystem paths, dependencies, and cache-busting versions — with automatic support for child-theme overrides, wp-scripts .asset.php metadata, and Laravel Mix manifests.

Why

WordPress asset registration usually means hardcoding URLs, manually tracking filemtime() for cache busting, and writing bespoke logic every time a child theme needs to override a parent theme or plugin asset. Hybrid Assets handles all of that:

  • One API for themes and plugins alike (path(), url(), assetUrl(), assetPath(), asset())
  • Automatic inheritance (when the inherit param is true) — child theme → parent theme → plugin fallback chain
  • Automatic versioning — reads wp-scripts .asset.php files or Laravel Mix mix-manifest.json, falling back to a content hash
  • Dependency resolution — reads wp_enqueue_script/style dependency arrays straight out of .asset.php

Requirements

Installation

composer require themehybrid/hybrid-assets

Register the service provider on your container:

$app->register( \Hybrid\Assets\AssetsServiceProvider::class );

This binds ParentTheme and ChildTheme as singletons, and Plugin as a fresh (non-shared) binding — each plugin carries its own config (plugin file, override directory, manifest settings), so instances can't be shared between consumers.

Setting up a resolver

AssetsServiceProvider registers the underlying ParentTheme / ChildTheme / Plugin classes, but you still bind your own named instance — configured for your theme or plugin's specific asset directory — in your provider's register() method.

In a theme

use Hybrid\Assets\ParentTheme;

$this->app->singleton( 'my-theme/assets', static function ( $app ) {
    /** @var ParentTheme $theme */
    $theme = $app->make( ParentTheme::class );
    $theme->setAssetsDirectory( '/assets' );
    $theme->setManifestDirectory( '/assets' );

    return $theme;
} );

In a plugin

use Hybrid\Assets\Plugin as AssetsPlugin;

$this->app->singleton( 'my-plugin/assets', static function ( $app ) {
    /** @var AssetsPlugin $plugin */
    $plugin = $app->make( AssetsPlugin::class );
    $plugin->setPluginFile( MY_PLUGIN_FILE );

    // By default, plugin asset overrides are expected under `{theme}/public/my-plugin/...`.
    // Themes can override this slug to match their own asset structure, such as `dist/`,
    // `assets/`, or any other custom directory.
    $plugin->setOverrideAssetsDirectory(
        apply_filters( 'my-plugin/assets/override/path', '/public/my-plugin' )
    );

    return $plugin;
} );

Binding key names (my-theme/assets, my-plugin/assets) are your choice — just keep them unique across the container and reuse the same string in your Facade accessor (see below).

Usage

Direct container access

$theme = app( 'my-theme/assets' );

$theme->url( '/js/app.js' );
$theme->path( '/js/app.js' );

Via a Facade (recommended)

Wrapping your bound instance in a Facade gives you a clean, static-style call site without losing testability. Define one per theme/plugin:

<?php

namespace MyTheme\Facades;

use Hybrid\Core\Facades\Facade;

/**
 * @method static string url(string $file)
 * @method static string path(string $file)
 * @method static string assetUrl(string $file, bool $inherit)
 * @method static string assetPath(string $file, bool $inherit)
 * @method static \Hybrid\Assets\Asset asset(string $file, bool $inherit, string $overrideManifestDirectory = '')
 */
class Assets extends Facade {

    protected static function getFacadeAccessor() {
        return 'my-theme/assets';
    }

}

Then enqueue assets with it directly:

/** @var \Hybrid\Assets\Asset $asset */
$asset = Assets::asset( 'js/my-file.js' );

wp_enqueue_script(
    'my-file',
    $asset->url(),
    $asset->dependencies(), // read from js/app.asset.php, if present
    $asset->version(), // from .asset.php, mix-manifest.json, or a content hash
    true
);

The same pattern applies to plugins — just point getFacadeAccessor() at your plugin's binding key:

<?php

namespace MyPlugin\Facades;

use Hybrid\Core\Facades\Facade;

/**
 * @see \Hybrid\Assets\Plugin
 *
 * @method static string url(string $file)
 * @method static string path(string $file)
 * @method static string assetUrl(string $file, bool $inherit)
 * @method static string assetPath(string $file, bool $inherit)
 * @method static \Hybrid\Assets\Asset asset(string $file, bool $inherit, string $overrideManifestDirectory = '')
 */
class Assets extends Facade {

    protected static function getFacadeAccessor() {
        return 'my-plugin/assets';
    }

}
/** @var \Hybrid\Assets\Asset $asset */
$asset = Assets::asset( 'js/my-file.js', true );

wp_register_script(
    'my-file',
    $asset->url(),
    $asset->dependencies(), // read from js/my-file.asset.php, if present
    $asset->version(),      // from .asset.php, mix-manifest.json, or a content hash
    true
);

Child-theme / plugin-override inheritance

Pass inherit: true as the second argument to check the inheritance chain first. This lets a child theme override a parent theme's asset, or a theme override a plugin's asset:

// Checks the child theme first, then falls back to the parent theme itself.
Assets::asset( 'js/my-file.js', true );

// For a plugin: checks child theme, then parent theme (both under the
// plugin's override directory), before falling back to the plugin's own file.
Assets::asset( 'js/my-file.js', true );

Laravel Mix support

If your build uses Laravel Mix instead of @wordpress/scripts, point Hybrid Assets at the manifest when you bind it:

$theme->setManifestDirectory( '/assets' );          // where mix-manifest.json lives
$theme->setManifestFileName( 'mix-manifest.json' );  // default, rarely needs changing

Metadata resolution order is always: .asset.phpmix-manifest.json → content hash fallback.

How resolution works

asset( $file, inherit: true )
  │
  ├─ inherit? ─── walk inheritance chain (child theme → parent theme → plugin)
  │                 └─ first resolver where the file exists & is readable wins
  │
  └─ resolve( $file )
        └─ new Asset( resolver, file, path )
              ├─ url()          → resolver->url( $file )
              ├─ path()         → resolver->path( $file )
              ├─ dependencies() → from .asset.php, else []
              └─ version()      → .asset.php → mix-manifest.json → md5_file() hash

Exceptions

All exceptions implement Hybrid\Assets\Contracts\AssetsException, so you can catch the whole package with one type if needed.

Exception Thrown when
InvalidAssetFileException asset() / resolve() is called with a blank file path
PluginFileNotSetException A Plugin resolver is used before setPluginFile()
PathOutsideBaseException A resolved .asset.php path escapes the resolver's base directory (traversal guard)
UnresolvableBaseDirectoryException The resolver's base directory can't be resolved via realpath()

Architecture

Class Role
Contracts\AssetsResolver Contract: path(), url(), asset(), assetUrl(), assetPath(), directory getters
AssetsResolver (abstract) Shared resolution logic (directories, inheritance chain, normalization) — extended by all three resolvers below
ParentTheme Resolves assets in the active parent theme
ChildTheme Resolves assets in the active child theme, if one exists
Plugin Resolves assets in a specific plugin; supports override directories
Asset Immutable, fully-resolved asset (URL, path, dependencies, version)
Concerns\AssetMetaData Trait: reads .asset.php / mix-manifest.json metadata, path-traversal safe
AssetsServiceProvider Registers the above with the Hybrid Core container

License

This project is licensed under the GNU GPL, version 2 or later.

2008 – 2026 © Theme Hybrid.