Summary
Wiring AssetMapper paths in third-party UX bundles requires repetitive, error-prone boilerplate. This proposal suggests adding a base class or trait to Symfony UX (or the HttpKernel bundle) so that bundle authors following standard conventions get correct AssetMapper configuration automatically.
Problem
Every Symfony UX bundle currently needs to implement prependExtension() manually:
public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void
{
$container->prependExtensionConfig('framework', [
'asset_mapper' => [
'paths' => [
__DIR__.'/../../assets' => '@survos/ux-calendar-bundle',
],
],
]);
}
This is fragile because:
- The relative path (
/../../assets/dist) must be maintained manually per-bundle and is easy to get wrong
- The asset namespace string is duplicated in multiple places
- There is no convention enforcement — every bundle author invents their own approach
- The
isAssetMapperAvailable() guard is typically omitted or reimplemented inconsistently
Proposed Solution
A reusable AssetMapperBundle base class (or trait) that encapsulates the standard wiring. Here is a working implementation used in production across several survos/* bundles:
<?php
declare(strict_types=1);
namespace Survos\CoreBundle\Bundle;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
abstract class AssetMapperBundle extends AbstractBundle
{
public function isAssetMapperAvailable(ContainerBuilder $container): bool
{
return interface_exists(AssetMapperInterface::class)
&& $container->hasExtension('framework');
}
public function getPaths(): array
{
$dir = realpath($this->getPath().'/assets');
assert($dir && file_exists($dir), 'assets path must exist: '.$this->getPath());
return [$dir => $this->getAssetNamespace()];
}
public function getAssetNamespace(): string
{
// Explicit constant takes highest priority
if (defined('static::ASSET_NAMESPACE')) {
return static::ASSET_NAMESPACE;
}
// Derive from ASSET_PACKAGE constant if defined
if (defined('static::ASSET_PACKAGE')) {
$package = static::ASSET_PACKAGE;
if (str_starts_with($package, '@')) {
return $package;
}
$package = preg_replace('#^survos/#', '', $package) ?? $package;
return '@survos/'.trim($package, '/');
}
// Fall back to deriving namespace from the bundle class name
$shortName = (new \ReflectionClass($this))->getShortName();
$shortName = preg_replace('/^Survos/', '', $shortName) ?? $shortName;
$shortName = preg_replace('/Bundle$/', '', $shortName) ?? $shortName;
$slug = strtolower((string) preg_replace('/(?<!^)[A-Z]/', '-$0', $shortName));
return '@survos/'.$slug;
}
public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void
{
if (!$this->isAssetMapperAvailable($builder)) {
return;
}
$builder->prependExtensionConfig('framework', [
'asset_mapper' => [
'paths' => $this->getPaths(),
],
]);
}
}
With this in place, a new UX bundle becomes:
class SurvosUxCalendarBundle extends AssetMapperBundle
{
public const ASSET_PACKAGE = 'ux-calendar';
}
That's it. No manual path wiring, no duplicated namespace strings.
Conventions assumed
| Convention |
Value |
| Assets directory |
<BundleRoot>/assets/ |
| Namespace derivation |
From ASSET_NAMESPACE, then ASSET_PACKAGE, then class name |
| Namespace format |
@vendor/package-name |
All of these are overridable by implementing the relevant method or constant.
Why a base class rather than a trait?
A trait would also work and may be preferable if bundle authors need to extend a different base class. Both approaches are viable — the key value is in the shared, tested logic for path resolution and namespace derivation.
Prior art / references
- This pattern is already in use across
survos/* bundles in production
- Related: Symfony UX contribution guide
- The
symfony-ux keyword requirement in composer.json for auto-discovery of UX controllers is a related underdocumented convention that this base class could help enforce or document
Impact
Low risk — this is purely additive. Existing bundles are unaffected. Third-party bundle authors gain a well-tested, conventional starting point that reduces a common source of subtle misconfiguration.
Summary
Wiring AssetMapper paths in third-party UX bundles requires repetitive, error-prone boilerplate. This proposal suggests adding a base class or trait to Symfony UX (or the HttpKernel bundle) so that bundle authors following standard conventions get correct AssetMapper configuration automatically.
Problem
Every Symfony UX bundle currently needs to implement
prependExtension()manually:This is fragile because:
/../../assets/dist) must be maintained manually per-bundle and is easy to get wrongisAssetMapperAvailable()guard is typically omitted or reimplemented inconsistentlyProposed Solution
A reusable
AssetMapperBundlebase class (or trait) that encapsulates the standard wiring. Here is a working implementation used in production across severalsurvos/*bundles:With this in place, a new UX bundle becomes:
That's it. No manual path wiring, no duplicated namespace strings.
Conventions assumed
<BundleRoot>/assets/ASSET_NAMESPACE, thenASSET_PACKAGE, then class name@vendor/package-nameAll of these are overridable by implementing the relevant method or constant.
Why a base class rather than a trait?
A trait would also work and may be preferable if bundle authors need to extend a different base class. Both approaches are viable — the key value is in the shared, tested logic for path resolution and namespace derivation.
Prior art / references
survos/*bundles in productionsymfony-uxkeyword requirement incomposer.jsonfor auto-discovery of UX controllers is a related underdocumented convention that this base class could help enforce or documentImpact
Low risk — this is purely additive. Existing bundles are unaffected. Third-party bundle authors gain a well-tested, conventional starting point that reduces a common source of subtle misconfiguration.