sprintf/symfony-metadata-loader
Brings Symfony's metadata loading components to Laravel, letting you define and resolve class metadata such as validation, serialization, and ORM mappings using familiar Symfony conventions. Includes service provider wiring and configuration for seamless integration.
Install and verify the shape of the package before writing any code.
composer require sprintf/symfony-metadata-loader
With 0 stars and 0 dependents there is no community documentation to lean on, so the first five minutes should be spent answering three questions:
composer.json and check autoload.psr-4 and for an extra.symfony block (Flex recipe). Look in src/ for a *Bundle.php. If there is no bundle, you will register services manually — that is the common case for a small "loader" library.*LoaderInterface / MetadataLoaderInterface and the object it returns (*Metadata, ClassMetadata, MetadataInterface). That interface is the package — everything else is plumbing. Read it before reading any implementation.AttributeLoader, YamlFileLoader, XmlFileLoader, ArrayLoader. This tells you whether you are annotating DTOs, shipping config files, or both.Where to look first, in order: src/*LoaderInterface.php → src/*Metadata.php → Tests/ (the only reliable usage examples in a package this young) → composer.json for symfony/* version constraints and minimum PHP.
First use case — prove the loader works in isolation before wiring it into the container. Write a throwaway test or bin/console command that loads metadata for one class and dumps it:
$loader = new AttributeLoader(); // or ArrayLoader / YamlFileLoader
$metadata = $loader->loadMetadata(MyDto::class);
dump($metadata);
If dump() gives you a populated metadata object, you understand the package. If it throws, the exception message will tell you more than the README will.
Only after that should you wire it into config/services.yaml and start consuming it from application code.
Because there is no Flex recipe (verify with composer show sprintf/symfony-metadata-loader), define services yourself and always alias against the package's interface. That gives you a one-line swap when the package is abandoned or replaced.
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
sprintf.metadata.loader.attribute:
class: Sprintf\SymfonyMetadataLoader\Loader\AttributeLoader
Sprintf\SymfonyMetadataLoader\Loader\LoaderInterface:
alias: sprintf.metadata.loader.attribute
Type-hint the interface in your own services, never the concrete loader.
Raw loaders do reflection on every call — fine in a test, wasteful in a request. The standard pattern (borrowed from Doctrine's metadata layer, which this package is almost certainly modelled on) is a two-layer stack: a factory that memoises, and a PSR-6/PSR-16 cache behind it.
final class CachedMetadataFactory
{
/** @var array<class-string, object> */
private array $loaded = [];
public function __construct(
private LoaderInterface $loader,
private CacheItemPoolInterface $cache,
) {}
public function get(string $class): object
{
if (isset($this->loaded[$class])) {
return $this->loaded[$class];
}
$key = 'meta_' . md5($class);
$item = $this->cache->getItem($key);
if (!$item->isHit()) {
$item->set($this->loader->loadMetadata($class));
$this->cache->save($item);
}
return $this->loaded[$class] = $item->get();
}
}
If the package ships its own factory class, use theirs and decorate it rather than reimplementing — but keep the decorator so cache policy stays yours.
ifsIf the package offers both attribute and file loaders, drive them through a composite so merging semantics live in one place. If it does not ship a chain loader, write a 20-line one implementing the same interface and iterate loaders in priority order:
final class ChainLoader implements LoaderInterface
{
/** @param LoaderInterface[] $loaders */
public function __construct(private iterable $loaders) {}
public function loadMetadata(string $class): object
{
$metadata = null;
foreach ($this->loaders as $loader) {
$metadata = $loader->merge($metadata, $loader->loadMetadata($class))
?? $loader->loadMetadata($class);
}
return $metadata;
}
}
Tag your loaders and inject them with !tagged_iterator metadata.loader — this is the one place where Symfony's autoconfiguration earns its keep, and it makes "add YAML overrides on top of attributes" a config change rather than a code change.
The biggest win in day-to-day use: pay reflection cost once at build, not per request.
final class MetadataCacheWarmer implements CacheWarmerInterface
{
public function __construct(
private CachedMetadataFactory $factory,
private array $classes, // from a container parameter or a scan of src/Dto
) {}
public function warmUp(string $cacheDir, ?string $buildDir = null): array
{
foreach ($this->classes as $class) {
$this->factory->get($class); // populates the shared pool
}
return [];
}
public function isOptional(): bool { return true; }
}
Populate $classes from an explicit list or a find src -name '*.php' scan in a build script — do not glob at runtime.
The realistic integration: your DTOs carry attributes, the loader turns them into a metadata object, and your validator/mapper/hydrator reads that object instead of reflecting directly. Keep the metadata object as the boundary — never leak the package's classes past your own service.
foreach ($factory->get(RegisterUserRequest::class)->getProperties() as $property => $rules) {
$mapper->addRules($property, $rules);
}
Skip the container in unit tests. Instantiate the loader, point it at a fixture class or fixture YAML file, assert on the metadata object. Keep one integration test that boots the container and asserts the alias resolves — that is what catches a broken services.yaml after an upgrade.
Treat the absence of dependents as a risk register entry. Zero stars, zero dependents, and a 0.03 score means: no one else has hit the edge cases, no Stack Overflow answers exist, and the API may change in a patch release. Pin the exact version ("sprintf/symfony-metadata-loader": "1.2.3") rather than using ^, and wrap it behind your own thin interface so replacement cost stays low.
LGPL-2.1 is not a blocker for a Symfony app, but it has one rule that bites. Dynamic use (composer dependency, linking) is fine. What is not fine is copying the package's source into your src/ and modifying it without publishing those modifications. If you need to fix a bug, prefer a decorator or a subclass, or fork it publicly — do not vendor-and-patch silently.
No Flex recipe is likely — expect manual config. Run composer show sprintf/symfony-metadata-loader and check the extra block. If extra.symfony is missing, Symfony will not auto-register anything; if a *Bundle class exists you must add it to config/bundles.php by hand, and if it doesn't, you must define services yourself. A silent "class not found" or "cannot autowire interface" on first boot is almost always this.
Cache staleness in development is the #1 debugging trap. File-based loaders key on class name, not file mtime. Edit a YAML metadata file and the cached entry survives. Disable the metadata pool in config/packages/dev/cache.yaml (or use cache.adapter.array) so metadata is rebuilt every request locally, and only enable the real pool in prod. Alternatively key the cache on filemtime() of every source file in dev.
Reflection cost is real under php-fpm with no cache. If you skip the factory/cache layer, you will re-reflect every DTO on every request. Measure with a simple microtime around a 100-class loop before deciding you can skip caching — for a handful of classes it genuinely doesn't matter.
Autoloading of metadata-bearing classes matters. Attribute loaders resolve ::class via reflection, which triggers autoloading of attribute classes. If you deploy with composer install --classmap-authoritative and forgot to dump after adding an attribute class, you get "class not found" at runtime in prod but not dev. Always run composer dump-autoload --optimize in the deploy pipeline.
Debugging: dump the resolved chain, not the exception. When metadata comes back empty, the problem is usually that the wrong loader resolved. Add a temporary command:
foreach ($this->container->get('metadata.loader') as $loader) {
dump($loader::class);
}
and bin/console debug:container --tag=metadata.loader (adjust the tag name to whatever the package's *Extension or your config uses). Also debug:container --show-private to confirm your alias points where you think it does.
Extension points worth knowing, in priority order:
Budget for a fallback. Give the integration a half-day spike and a clear abort criterion: if the metadata object doesn't expose what your consumer needs and there's no extension point to add it, write the 100 lines yourself against ReflectionClass. A zero-adoption dependency should earn its place in composer.json on every upgrade, not just the first one.
How can I help you explore Laravel packages today?