symfony/annotations-pack
Symfony annotations-pack is a Composer metapackage that pulls in the libraries needed to use annotations in Symfony applications, chiefly doctrine/annotations. It simplified dependency setup during the Symfony 3.4 to 4.x transition.
symfony/annotations-pack is a Composer metapackage — it contains no PHP code, no namespace, no services. It exists so that composer require annotations (via Symfony Flex) pulls in a working, cache-backed annotation stack in one step. Its only job is to require:
doctrine/annotations (the actual Reader implementation)symfony/cache (the PSR-6 pool used to cache parsed docblocks)Install
# If symfony/flex is present, the alias resolves to this pack
composer require annotations
# Otherwise, install the two things the pack would have installed
composer require doctrine/annotations symfony/cache
Where to look first
vendor/symfony/annotations-pack/composer.json — confirms there is nothing else in here.vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/ — AnnotationReader, PsrCachedReader, AnnotationRegistry.bin/console debug:container annotations.cached_reader and debug:container cache.annotations.First use case
Drop an annotation on something that already supports one — a route, a validator constraint, a Doctrine mapping — and confirm it is read through the cached reader rather than the raw one:
use Symfony\Component\Routing\Annotation\Route;
class HealthController
{
/**
* @Route("/health", name="health")
*/
public function check(): Response
{
return new Response('ok');
}
}
Then verify the cache is actually wired:
bin/console debug:container annotations.cached_reader --show-arguments
# expect: Doctrine\Common\Annotations\PsrCachedReader
# arguments: [@annotations.reader, @cache.annotations, %kernel.debug%]
If that service is missing or is a plain AnnotationReader, the pack was never installed (or you are on Symfony 7, where it is gone — see Gotchas).
FrameworkBundle wires the whole graph. The useful service IDs:
| Service ID | Class |
|---|---|
annotations.reader |
Doctrine\Common\Annotations\AnnotationReader |
annotations.cached_reader |
Doctrine\Common\Annotations\PsrCachedReader |
Doctrine\Common\Annotations\Reader (alias) |
the cached reader |
Inject the interface, never the concrete class, so tests can swap it:
use Doctrine\Common\Annotations\Reader;
final class EndpointDescriber
{
public function __construct(private Reader $reader) {}
public function describe(string $class, string $method): array
{
$ref = new \ReflectionMethod($class, $method);
return [
'route' => $this->reader->getMethodAnnotation($ref, Route::class),
'class' => $this->reader->getClassAnnotation(
new \ReflectionClass($class),
SomeMarker::class
),
'props' => $this->reader->getPropertyAnnotations(
new \ReflectionProperty($class, 'id')
),
'all' => $this->reader->getMethodAnnotations($ref),
];
}
}
This is the pattern the pack is really shorthand for. Build the decorator yourself:
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\PsrCachedReader;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
$pool = new FilesystemAdapter('annotations', 0, __DIR__ . '/var/cache');
$reader = new PsrCachedReader(new AnnotationReader(), $pool, $debug = false);
$route = $reader->getClassAnnotation(
new ReflectionClass(SomeController::class),
Route::class
);
PsrCachedReader is a straight decorator: cache key = class/method/property name + annotation class, so you never invalidate by hand in production.
namespace App\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* @Annotation
* @Target({"CLASS", "METHOD"})
*/
final class Audited
{
/** @var string */
public $channel = 'default';
public function __construct(array $values = [])
{
$this->channel = $values['channel'] ?? $values['value'] ?? 'default';
}
}
/**
* @Audited(channel="payments")
*/
final class ChargeCustomer { /* ... */ }
Two hard requirements: the @Annotation docblock tag on the annotation class, and a use statement at the usage site (use App\Annotation\Audited;). Both failure modes are silent-ish — see Gotchas.
# config/packages/framework.yaml
framework:
annotations:
cache: cache.annotations # pool name or service id
debug: '%kernel.debug%'
Prod: back cache.annotations with APCu or Redis and warm it during deploy (just run the app's routing/DI warmup — annotation entries are populated lazily on first reflect, so a cache:warmup plus one request per route set is the practical pattern). Dev: debug: true is what saves you from stale entries; don't disable it locally to "speed things up".
Annotations are resolved at runtime against docblocks, so tests that boot the kernel inherit whatever pool the environment uses. In config/packages/test/framework.yaml:
framework:
annotations:
cache: cache.adapter.array
This prevents filesystem cache entries from a previous run leaking into a test that asserts on a changed annotation.
There is no conflict — doctrine/annotations only reads docblocks; PHP reads #[...] natively. During migration you can read both from your own code:
$route = $ref->getAttributes(Route::class)[0]?->newInstance()
?? $this->reader->getMethodAnnotation($ref, Route::class);
Prefer attributes in new code; annotations only where a third-party bundle still requires them.
The pack is abandoned and its scope shrank with every Symfony release. Symfony 6.4 deprecated the framework.annotations config node and the annotation loaders for routing/serializer; Symfony 7 removed them. Doctrine ORM 3 removed its annotation driver entirely. If you are starting fresh on Symfony 7 / ORM 3, this pack is a dead end — require doctrine/annotations directly only if a specific third-party bundle still needs a Reader. Check composer why doctrine/annotations before adding anything.
"Package annotations not found." composer require annotations is a Flex alias; without symfony/flex installed Composer looks for a literal package named annotations on Packagist and fails. Install the two real packages instead.
@Annotation is not optional. A custom annotation class without the @Annotation docblock tag is treated as a plain class and simply never matches — getClassAnnotation() returns null with no error. This is the single most common "my annotation does nothing" bug.
Missing use statements produce confusing errors. With AnnotationReader in strict mode you get [Semantical Error] The annotation "@Audited" in ... was never imported. Did you maybe forget to add a "use" statement?. Note the reader resolves docblock names against the imports of the annotated file, not the annotation file — copy-pasting a docblock between files is a classic way to break it.
Stale cache in dev is a real time sink. If %kernel.debug% is false — common when someone sets APP_DEBUG=0 locally or copies a prod .env for a quick test — PsrCachedReader skips the mtime checks and you will edit an annotation for twenty minutes wondering why nothing changes. Symptom: bin/console cache:clear "fixes" it. Always confirm debug: true in dev before debugging anything else.
AnnotationRegistry::registerLoader() is gone in doctrine/annotations 2.0. Old snippets still float around; in 2.x, annotation classes are autoloaded normally. If you follow a StackOverflow answer that calls AnnotationRegistry, you are on 1.x or reading stale advice.
PsrCachedReader caches by (class, member, annotationClass) — not by file. Editing the annotation class itself (adding a property, changing its constructor) does not invalidate existing entries under debug: false. Clear the pool on deploy, not just application code.
Do not put the annotation pool on cache.app. It gets cleared by unrelated application cache operations and, worse, shares a namespace with data you may flush intentionally. cache.annotations (backed by cache.system) or a dedicated pool is correct.
Watch reflection cost in hot paths. $reader->getMethodAnnotations() materialises every annotation on a member, instantiating each one. If you only need one, use getMethodAnnotation($ref, Foo::class). In tight loops, resolve once at container-build time (a compiler pass) rather than per request.
Annotation targets are enforced, but late. @Target({"METHOD"}) violations throw AnnotationException at read time, not compile time — meaning a bad annotation on an unexercised code path stays hidden until that path runs. A cheap guard: a small test that reflects over your src/ annotation consumers.
Extending the reader is the supported escape hatch. AnnotationReader is final in 2.0 in most respects, but Reader is an interface — decorate annotations.cached_reader (Symfony's service decoration) if you need to log, normalise, or layer a second source of metadata. Keep the decorator outside the cached one, or you will re-parse docblocks on every call.
How can I help you explore Laravel packages today?