Compile-time discovery of controllers, routes, and entities — the atlas the rest of survos draws maps from.
composer require survos/atlas-bundleAt compile time, it walks:
- Controllers — every service tagged
container.service_subscriber(the Symfony default forAbstractControllersubclasses), recording oneRouteEntryper#[Route]method along with every other attribute on the method and class. - Entities — directories from
doctrine.orm.mappingsplussrc/Entityplus each registered bundle'sEntity/subdir, recording oneEntityEntryper concrete class with its class-level attributes.
The result is exposed as a runtime Atlas service. Atlas does not interpret attributes — it records them. Bundles like survos/field-bundle layer domain meaning (#[EntityMeta], #[RouteMeta], …) on top.
Attribute payloads are kept as plain arrays, not instantiated objects:
[
'class' => 'App\Attribute\PublicApi',
'args' => ['version' => 2, 'description' => 'Public list endpoint'],
]args is exactly what ReflectionAttribute::getArguments() returned: positional values keyed by integer, named values keyed by string name.
This shape has three properties worth keeping in mind:
- Trivially serializable.
atlas:exportis a one-linejson_encodebecause there are no objects to coerce. - Resilient. A renamed or removed attribute class will not break the Atlas; only consumers that explicitly look for that FQCN are affected.
- Composable. Multiple bundles can read the same
RouteEntryand each pull out the attributes they care about, without any of them needing to instantiate the others' classes.
To turn a stored entry back into a real attribute object, unpack the args:
foreach ($route->attributesOf(\App\Attribute\PublicApi::class) as $hit) {
$attr = new \App\Attribute\PublicApi(...$hit['args']);
}PHP's named-argument unpacking handles mixed positional/named args correctly, so this works whether the attribute was written as #[PublicApi(2)] or #[PublicApi(version: 2)].
use Survos\AtlasBundle\Service\Atlas;
final class MyService
{
public function __construct(private readonly Atlas $atlas) {}
public function example(): void
{
foreach ($this->atlas->routes() as $route) {
// $route->name, $route->path, $route->methodAttributes, ...
}
$hits = $this->atlas->routesWithAttribute(\App\Attribute\PublicApi::class);
}
}use Survos\AtlasBundle\Compiler\ControllerAtlasBuilder;
use Survos\AtlasBundle\Compiler\EntityAtlasBuilder;
final class MyBundlePass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
foreach (ControllerAtlasBuilder::build($container) as $route) {
foreach ($route->attributesOf(\App\Attribute\Foo::class) as $hit) {
// $hit['args'] holds the original attribute arguments
}
}
foreach (EntityAtlasBuilder::build($container, extraDirs: ['/path/to/extra']) as $entity) {
// ...
}
}
}The builders are pure helpers — no service registration, no runtime cost when called from a compile pass.
bin/console atlas:export # JSON to stdout
bin/console atlas:export --pretty # pretty JSON
bin/console atlas:export -f yaml # YAML
bin/console atlas:export -o atlas.json # write to fileThe output is the natural shape to paste into an LLM conversation when you want to ask design questions about your application's surface area.
- A
#[Route]without an explicitname:is skipped. Atlas needs a stable identifier and won't guess. - Multiple
#[Route]attributes on the same method each produce their ownRouteEntry. - Entity discovery skips abstract classes, interfaces, and traits.
Atlas is opinionated. Only attributes whose class FQCN starts with one of these prefixes are captured:
Survos\— every survos attribute (RouteMeta, EntityMeta, RouteIdentity, Field, MeiliIndex, Facet, …)Symfony\Component\Routing\Attribute\—#[Route]Symfony\Component\Security\Http\Attribute\—#[IsGranted]Symfony\Bridge\Twig\Attribute\—#[Template]App\Attribute\— your own attributes
Anything else — most importantly ApiPlatform's #[ApiResource] and friends — is silently filtered out. ApiPlatform's attribute universe is huge and deeply nested; analyzing it belongs in survos/inspection-bundle, not here. Atlas's scope is "metadata that drives survos features," not "every attribute on every method."
If you genuinely need wider coverage, pass extra prefixes to AttributeFilter::accepts($fqcn, $extraNamespaces) from your own builder call — but reconsider the layering first.
Atlas is intentionally low-level — it records what's there. If you want opinionated metadata attributes with semantic helpers on top, install survos/field-bundle:
composer require survos/field-bundlefield-bundle defines:
#[EntityMeta]— class-level metadata for admin UI, dashboards, menu auto-registration (icon, group, label, …)#[RouteMeta](in progress) — method-level metadata for sitemap inclusion, AI introspection, breadcrumb construction, …
Both are discovered through Atlas, and field-bundle ships richer registries (EntityMetaRegistry, RouteMetaRegistry) plus an enriched meta:export command that joins entities and routes into a single graph. Use Atlas directly when you have your own attribute vocabulary; reach for field-bundle when its vocabulary is what you'd be inventing.