mi/puli-metadata-file-locator-bundle
Symfony bundle that integrates Puli's metadata file locator, letting you resolve and locate package resource files through Puli's metadata-driven path system. It wires the locator into Symfony's service container so bundles and libraries can discover assets and configuration reliably.
Confirm you qualify before typing composer require. This is a Symfony bundle, not a Laravel package. Its only job is to replace Symfony's file_locator with one that resolves Puli resource paths (/acme/blog/doctrine/Post.orm.xml) through the Puli metadata repository. If you're on Symfony 4+, Laravel, or you don't already run Puli, stop here — there is no integration point, and Symfony removed its Puli integration in 4.0.
Assumed starting state
AppKernel, app/config/config.yml)webmozart/puli and its Composer plugin available (the bundle does not install Puli for you)Install
composer require mi/puli-metadata-file-locator-bundle
composer require webmozart/puli # if not already present
puli build # generate/refresh the .puli repository
puli status # confirm packages were discovered
Register the bundle in app/AppKernel.php. Keep it with your other infrastructure bundles, before app bundles whose metadata you want resolved through Puli:
$bundles = [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Mi\PuliMetadataFileLocatorBundle\MiPuliMetadataFileLocatorBundle(),
new Acme\BlogBundle\AcmeBlogBundle(),
];
There is no bundle configuration to write — the description's "no extra configuration beyond bundle registration" is accurate. All actual configuration lives in puli.json, not in config.yml.
Where to look first
app/console debug:container file_locator --show-private — the class should no longer be Symfony\Component\Config\FileLocator. If it is, the compiler pass didn't fire (bundle not registered, or the puli service isn't available).puli.json at project root — this is the resource manifest. Understand it before you understand anything else.puli status — shows which packages were discovered and whether bindings are installed.vendor/mi/puli-metadata-file-locator-bundle/ — at 0 stars, 0 dependents, there is no documentation beyond the source. Read DependencyInjection/ first; the bundle is small enough to audit in an afternoon.First use case: stop hard-coding Doctrine mapping paths
Before:
# app/config/config.yml
doctrine:
orm:
mappings:
AcmeBlog:
type: yml
dir: "%kernel.root_dir%/../src/Acme/BlogBundle/Resources/config/doctrine"
prefix: Acme\Blog\Model
After:
doctrine:
orm:
mappings:
AcmeBlog:
type: yml
dir: /acme/blog/doctrine
prefix: Acme\Blog\Model
The metadata loader asks file_locator for /acme/blog/doctrine, the Puli-backed locator resolves it through the repository to a physical path, and Doctrine loads the mappings from wherever the repository says they live. That indirection is the entire value proposition — verify it works for one mapping before expanding.
Pattern 1 — Doctrine mapping roots as Puli paths. The highest-value, lowest-risk adoption. Replace %kernel.root_dir%-relative paths in doctrine.orm.mappings and doctrine_mongodb mapping configs with Puli paths. Mappings are loaded at compile/warmup, so failures are loud and early, and the blast radius is contained to persistence metadata.
Pattern 2 — Cross-package validator/serializer/form metadata. Puli's real differentiator: a vendor package can ship /acme/blog/validation.yml and your application references that logical path without knowing where Composer installed the package:
# app/config/config.yml
framework:
validation:
mapping:
paths:
- /acme/blog/validation
This is the pattern to spike if your roadmap question is "should third-party packages contribute their own metadata?" Answer it here before designing a contract around it.
Pattern 3 — Environment-portable metadata roots. Bind the same logical path to different physical directories per environment in puli.json (a read-only container path in prod, a bind-mounted overlay in staging, an on-prem tenant directory for air-gapped customers). The application config never changes. This is the pattern that justifies the abstraction for deployment-target portability.
Pattern 4 — Resolve Puli paths in your own services. The locator is a normal service; inject it wherever you'd inject Symfony's. FileLocatorInterface::locate($name, $currentPath, $first) returns the resolved absolute path, so any service that currently takes a filesystem path can accept a Puli path instead:
class ReportTemplateRepository
{
public function __construct(private FileLocatorInterface $locator) {}
public function load(string $puliPath): string
{
return file_get_contents($this->locator->locate($puliPath));
}
}
Keep this usage to metadata/config resolution. Do not route request paths or security decisions through Puli.
Pattern 5 — Parallel resolution during migration. Point some mapping roots at Puli paths and leave the rest on filesystem paths. The bundle decorates rather than replaces file_locator's filesystem behaviour, so both work side by side. This lets you migrate one package at a time and roll back individual mappings without touching the bundle registration.
Pattern 6 — Explicit test assertion on resolution order. Because metadata resolution is cached at container warmup, add a smoke test that boots the kernel and asserts a known Puli path resolves to the expected physical file:
public function testPuliPathResolves(): void
{
$locator = static::createClient()->getContainer()->get('file_locator');
$this->assertFileExists($locator->locate('/acme/blog/doctrine'));
}
Without this, a silent fallback to filesystem paths will pass every other test in your suite while defeating the abstraction entirely.
Pattern 7 — Time-box behind a feature flag, then decide. The pragmatic day-to-day use is a one-to-two day spike on an isolated branch: resolve a single validator metadata file through Puli, prove the failure mode is observable, then decide fork-vs-reimplement. If you continue, immediately wrap Puli paths behind your own thin locator interface so you can swap the implementation later without rewriting consumer config.
Compiler pass order is the #1 source of "nothing happened." If debug:container file_locator shows the vanilla Symfony class, the pass didn't run. Check bundle registration order, then verify the puli service exists — a missing webmozart/puli install produces a container compile error or a silently skipped pass depending on version.
puli.json is the config surface, not config.yml. Symfony config knows nothing about resource bindings. When a path fails to resolve, the fix is almost always in puli.json or a stale .puli directory, not in your bundle configuration.
puli build must run in your deploy pipeline. The .puli repository is generated state. Commit it or generate it — pick one and be consistent. If it's generated locally but not in CI, you get "works locally, fails in CI" with container compile errors that are opaque.
Beware the silent fallback. If the locator can't resolve a Puli path it may fall through to filesystem resolution rather than throwing. Misconfiguration then passes all tests while loading metadata from a stale hard-coded location. Assert resolution explicitly in one test and log which locator handled each metadata root during warmup.
Cache warmup divergence across environments. Metadata is resolved once at compile/warmup and cached. A cache built in dev against a dev-only overlay directory will resolve differently in prod. Clear and rebuild caches per environment in CI; don't promote a var/cache artifact across environments.
The Puli Composer plugin mutates install behaviour. CI reproducibility and offline builds are affected. Pin the plugin version, document the install order, and expect composer install --no-plugins to break resource discovery.
Composer resolution will likely fight you on any framework upgrade. Legacy symfony/* constraints and PHP version floors mean this bundle is effectively pinned to Symfony 2.8/3.x. That's fine inside a legacy app and fatal in a greenfield one — it isn't a dependency you upgrade, it's a dependency you migrate off.
Nobody is coming to help you. Zero stars, zero dependents, zero forks. There is no issue tracker triage, no security advisories, no downstream validation of a breaking edge case. Budget from day one for a vendored copy or private Composer repository, a named internal owner, and reading Puli's source when resolution fails. Treat "0 dependents" as "nobody else is paying this tax."
Debugging checklist, in order: puli status → puli.json bindings → debug:container file_locator → clear app/cache and re-warm → check .puli freshness. Only then read the bundle source.
Extension point is narrow by design. The seam is file_locator, so you can decorate Puli's locator further for your own naming convention, but you cannot extend the bundle to cover routing or security — and you shouldn't try. If you need more, that's a signal to build a custom FileLocatorInterface decorator instead of adopting this dependency.
Don't leak Puli paths into public package contracts. Once a package ships a Puli path in its config, you owe backward compatibility on an abstraction maintained by nobody. Keep Puli paths internal to your application config until you've explicitly committed to owning that contract — and if you have, you're forking the bundle anyway, so write down the maintainer's name in the repo first.
How can I help you explore Laravel packages today?