incenteev/hashed-asset-bundle
incenteev/hashed-asset-bundle is a Symfony bundle for cache-busting static assets by generating and resolving hashed filenames. It helps serve versioned CSS, JavaScript, and images with long-lived browser caching while keeping templates clean and deployments predictable.
The bundle slots into Symfony's Asset component as a VersionStrategyInterface implementation: instead of one global version string, every asset gets a version derived from the hash of its file contents. asset('css/app.css') becomes css/app.css?v=9f2c… and the query string changes only when the file changes.
Minimal setup:
composer require incenteev/hashed-asset-bundle
Enable the bundle (Symfony 4+/Flex):
// config/bundles.php
return [
// ...
Incenteev\HashedAssetBundle\IncenteevHashedAssetBundle::class => ['all' => true],
];
Configure the strategy options (algorithm is the main knob; the bundle also exposes format/length options — read DependencyInjection/Configuration.php in the vendor dir for the authoritative list):
# config/packages/incenteev_hashed_asset.yaml
incenteev_hashed_asset:
algorithm: sha1 # md5, sha1, xxh128, etc.
Wire it as the version strategy. The bundle registers a version-strategy service; reference that service id (confirm with bin/console debug:container incenteev — it is typically incenteev_hashed_asset.version_strategy):
# config/packages/framework.yaml
framework:
assets:
version_strategy: incenteev_hashed_asset.version_strategy
First use case: render {{ asset('css/app.css') }}, confirm the ?v=<hash> suffix, edit the file, reload, and confirm the hash changed. If it did, everything else is a matter of scoping (packages) and deployment.
Where to look first:
DependencyInjection/Configuration.php — the real API surface.Global default strategy. Set framework.assets.version_strategy once and every asset() call in the default package is hashed. This is the right default for a small app with a public/ (or web/) directory of hand-managed CSS/JS/images.
Per-package strategies. The strategy is a package-level concern, so use named packages to keep concerns separate:
framework:
assets:
version_strategy: incenteev_hashed_asset.version_strategy
packages:
encore: # Webpack Encore output — leave to the manifest strategy
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
vendor:
base_path: '/vendor'
version_strategy: incenteev_hashed_asset.version_strategy
Then in Twig: {{ asset('js/app.js', 'encore') }} vs {{ asset('css/app.css') }}. Don't route Encore output through this bundle; Encore already versions filenames via its manifest.
Hashing compiled assets. The hash is of the file on disk after install. If you compile Sass/JS, compile first, then bin/console assets:install --symlink (or copy), then let the strategy read the installed file. In CI, the order is: build → install → deploy → warm cache.
Query-string format. If you want ?h= instead of ?v=, set the bundle's format option (placeholders are %s; escape as %%s?%%s in Symfony YAML). Keep in mind this bundle only appends a version — it does not rewrite filenames to app.abc123.css. If you need filename-based versioning, use a manifest strategy instead.
Absolute URLs and CDNs. Versioning happens before absolute_url(), so absolute_url(asset('img/x.png')) is safe. Hash is computed from the local file; the URL host comes from base_urls. As long as the local file exists and matches what the CDN serves, this works with a CDN.
Testing. Assert output shape with a regex (/\.css\?v=[0-9a-f]+$/), and unit-test the strategy directly against a temp file if you extend it. Ensure assets are installed in the test environment's web dir in CI; otherwise hashing fails.
Extension points. The strategy is a plain service: decorate it (decorates: incenteev_hashed_asset.version_strategy) to add APCu/Redis memoization, logging, or per-extension behavior (e.g., hash only .css, .js, images). Alternatively, implement VersionStrategyInterface yourself for manifest lookups or filename rewriting.
public/, on Symfony 2/3 it's web/. If the bundle's lookup path doesn't match your layout, hashing fails or silently skips. Check the config option for the web/base dir and point it at %kernel.project_dir%/public.prod.version and version_strategy together. Symfony throws a LogicException. Package-level version creates a static strategy and overrides whatever the default is.asset() call unless the strategy memoizes. Small asset counts are fine; large pages with dozens of assets add file I/O. Realpath/OPcache caches help with stat calls but not with content hashing. If it matters, decorate the strategy with a cache keyed on path + mtime.url() references are not rewritten by the Asset component at all. Query-string versioning of app.css does not bust images referenced inside it — those keep their own URLs. Reference bustable images through Twig, or move to a manifest strategy.curl -I against the CDN edge.assets:install before traffic shifts.hash_length trade-off. Truncated hashes are fine for cache-busting but increase collision risk. Anything ≥ 8 hex chars is practically safe; don't go to 4.assets:install --symlink works fine as long as the symlink resolves and the file is readable by the PHP process (permissions in containers are a common trap).bin/console debug:container incenteev to confirm the service id and tags; bin/console debug:config incenteev_hashed_asset if the bundle supports config dumping; clear var/cache after config changes; view-source (not the network tab, which may cache) to confirm the query string actually changes.composer.json constraints before a major Symfony upgrade. If it lags, the strategy class is small enough to copy into your app as a custom VersionStrategyInterface implementation.prod will behave differently in dev/test. Keep the file lookup purely config-driven so all environments agree.How can I help you explore Laravel packages today?