Created
September 8, 2026 12:07
-
-
Save dbu/aca107c0c42929762d7ddcda6d7703ee to your computer and use it in GitHub Desktop.
use asset mapper along with regular assets that need a static version
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| declare(strict_types=1); | |
| use Symfony\Component\Asset\PackageInterface; | |
| use Symfony\Component\Asset\PathPackage; | |
| use Symfony\Component\Asset\UrlPackage; | |
| use Symfony\Component\AssetMapper\AssetMapperInterface; | |
| use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; | |
| use Symfony\Component\HttpFoundation\RequestStack; | |
| /** | |
| * Workaround to run AssetMapper along with other assets that need a version configured. | |
| * | |
| * This problem was discussed in https://github.com/symfony/symfony/issues/57584 but the issue has been closed | |
| * without a solution. | |
| * | |
| * Replacement for Symfony's own `asset_mapper.asset_package` (MapperAwareAssetPackage), which unconditionally | |
| * forwards to the decorated default package's getUrl() - including its version strategy - even for paths | |
| * AssetMapper itself just resolved to a content-hashed file. With framework.assets.version enabled, that | |
| * stacks a second, static "?%version%" on top of the hash (…-PEf0l1g.js?2.2) and 404s, because the extra | |
| * suffix is not part of the file that was compiled. | |
| * | |
| * Fix: only fall back to the inner (versioned) package for paths AssetMapper does NOT manage. Mapped paths | |
| * are already uniquely versioned by their content hash and are returned as-is. | |
| * | |
| * A path counts as AssetMapper-managed in two cases: | |
| * 1. `assetMapper->getPublicPath($path)` resolves it - $path is a logical path | |
| * (e.g. "@Ui/controller/select_controller.js"), when asset() is called directly. | |
| * 2. $path already starts with the AssetMapper public prefix (e.g. "assets/…") - it is already the resolved, | |
| * hashed physical path, so getPublicPath() (which only indexes logical paths) returns null even though | |
| * this IS an AssetMapper file. This happens for every importmap entry: ImportMapRenderer resolves each | |
| * entry to its hashed path itself, then again runs that hashed path back through this same default | |
| * package (to support subdirectory/CDN prefixing) - so without this second check, every Stimulus | |
| * controller URL gets double-versioned again despite the first check. | |
| */ | |
| final class AssetMapperCompatiblePackage implements PackageInterface | |
| { | |
| private readonly ?string $devServerPrefix; | |
| private readonly string $assetMapperPublicPrefix; | |
| public function __construct( | |
| private readonly PackageInterface $innerPackage, | |
| private readonly AssetMapperInterface $assetMapper, | |
| PublicAssetsPathResolverInterface $publicAssetsPathResolver, | |
| private readonly ?RequestStack $requestStack = null, | |
| ?string $devServerPublicPrefix = null, | |
| ) { | |
| $this->devServerPrefix = null === $devServerPublicPrefix ? null : '/'.trim($devServerPublicPrefix, '/').'/'; | |
| $this->assetMapperPublicPrefix = $publicAssetsPathResolver->resolvePublicPath(''); | |
| } | |
| public function getVersion(string $path): string | |
| { | |
| // Already versioned via the content hash baked into the mapped path - no extra version needed. | |
| return $this->isAssetMapperManaged($path) ? '' : $this->innerPackage->getVersion($path); | |
| } | |
| public function getUrl(string $path): string | |
| { | |
| $publicPath = $this->assetMapper->getPublicPath($path); | |
| if (null === $publicPath && !$this->isAssetMapperManaged($path)) { | |
| return $this->innerPackage->getUrl($path); | |
| } | |
| $path = ltrim($publicPath ?? $path, '/'); | |
| if (null !== $this->devServerPrefix && str_starts_with('/'.$path, $this->devServerPrefix)) { | |
| // the dev server serves those assets through the kernel, so the front controller must be part of the URL | |
| $request = $this->requestStack?->getMainRequest(); | |
| $frontController = $request ? trim(substr($request->getBaseUrl(), \strlen($request->getBasePath())), '/') : ''; | |
| if ('' !== $frontController) { | |
| $path = $frontController.'/'.$path; | |
| } | |
| } | |
| // Deliberately does not call $this->innerPackage->getUrl($path): that would re-apply the default | |
| // package's version strategy on top of the hash. Still prepends whatever base_path/base_urls the | |
| // inner package is configured with, without going through its version strategy. | |
| return $this->prependBase($path); | |
| } | |
| private function isAssetMapperManaged(string $path): bool | |
| { | |
| return null !== $this->assetMapper->getPublicPath($path) | |
| || str_starts_with('/'.ltrim($path, '/'), $this->assetMapperPublicPrefix); | |
| } | |
| /** | |
| * Mirrors PathPackage/UrlPackage::getUrl() minus the version strategy - those are the only two classes | |
| * Symfony's FrameworkExtension ever wires up as the default package. Falls back to a plain root-relative | |
| * path for any other implementation (there is none to mirror version-less prefixing for). | |
| */ | |
| private function prependBase(string $path): string | |
| { | |
| if ($this->innerPackage instanceof UrlPackage) { | |
| return $this->innerPackage->getBaseUrl($path).'/'.$path; | |
| } | |
| if ($this->innerPackage instanceof PathPackage) { | |
| return $this->innerPackage->getBasePath().$path; | |
| } | |
| return '/'.$path; | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
FTR: this has been integrated into symfony in a proper way in symfony/symfony#65911