ibrows/annotation-reader
ibrows/annotation-reader parses PHP docblock annotations into structured objects, letting you read class, method and property metadata at runtime via reflection. A lightweight API for annotation scanning, useful for custom routing, ORMs, validation or framework features in Laravel and plain PHP proj...
Know what you're installing. This is a plain Composer library, not a Laravel package. There is no service provider, no facade, no config/annotation-reader.php to publish, and no auto-discovery. You wire it up yourself. With 0 stars and 0 dependents, the README.md and vendor/ibrows/annotation-reader/src/ are the documentation — read both before writing a line of consuming code, and confirm the actual class/method names rather than trusting snippets (including these).
Step 1 — install and let Composer tell you if it's viable.
composer require ibrows/annotation-reader
If Composer refuses on your PHP version, that's your answer: stop and move to native PHP 8 attributes (ReflectionClass::getAttributes()). If it installs, immediately inspect composer.lock — the library historically leans on Doctrine-style docblock parsing, so check whether doctrine/annotations / doctrine/common arrived transitively. That transitive dependency is often the real parser.
Step 2 — spike one class, one annotation. Do this in a throwaway branch with the exact PHP/Laravel version you'll ship on. Confirm the reader constructs and returns something before designing anything around it:
/**
* @Config(key="sync", ttl=300)
*/
class OrderSyncer {}
// Verify the constructor signature in vendor/ before copying this.
$reader = app(\Ibrows\AnnotationReader\Reader::class);
dump($reader->getClassAnnotations(OrderSyncer::class));
Step 3 — bind it once. Register the reader as a singleton in AppServiceProvider::register() so parsing is not repeated on every resolution. This single decision separates a usable integration from an unusable one.
First real use case. The lowest-risk, highest-value job is turning a class-level annotation into a config array at boot — e.g. a FeatureFlags class whose annotations are read once into config(). It's small, testable, and trivially replaceable later with attributes, so you get the shape of the integration without betting on the library.
Wrap it behind an application-owned interface. Never let controllers, models, or services call the package directly. Define App\Metadata\MetadataReader with methods like classMetadata(string $class): array, and implement it with the package. Everything downstream depends on your interface, not on an abandoned dependency — this is the seam that makes migration to attributes a one-file change later.
Singleton binding (the usual integration shape).
$this->app->scoped(\App\Metadata\MetadataReader::class, function () {
return new \App\Metadata\AnnotationMetadataReader(
new \Ibrows\AnnotationReader\Reader(
new \Doctrine\Common\Annotations\AnnotationReader()
)
);
});
Annotation objects as DTOs. Define your own annotation classes with public properties (or a constructor that validates), and let the parser hydrate them. This gives you typed, IDE-friendly consumers and lets you fail loudly on missing required parameters inside the DTO rather than discovering null mid-request.
Cache parsed metadata — always. Parsing docblocks via reflection costs milliseconds per class; across dozens of models per request it dominates. Read once and store:
$meta = Cache::rememberForever(
'annotations:'.$class.':'.filemtime((new ReflectionClass($class))->getFileName()),
fn () => app(\App\Metadata\MetadataReader::class)->classMetadata($class)
);
Key by class plus file mtime (or flush on deploy) so edits invalidate cleanly. A plain $class key silently serves stale metadata after a refactor — one of the most confusing bugs you'll hit.
Prefer a build-time metadata dump. A small artisan annotations:cache command that walks your scan paths, reads annotations, and writes a single PHP array or cache entry is the most robust pattern. Requests then do array lookups, not reflection. It also gives you one place to log malformed annotations.
Validation layer. The raw reader is permissive — unknown or malformed annotations are typically ignored rather than throwing. Wrap it in an AnnotationRegistry that (a) reads, (b) asserts required annotations/properties, (c) throws a domain exception. Surface failures at boot in development, not at 2am in production.
Handler/plugin discovery. Method-level annotations work well for declaring handlers — e.g. @Handler(event="order.paid") on a service method — with a boot-time or build-time scan building a map. Keep the scan list explicit and small; do not crawl app/ on every request.
Testing. Bind a fake MetadataReader returning pre-built annotation DTOs for all behavioural tests. Keep one or two integration tests against the real parser so the fragile third-party code stays quarantined and its breakage is localized when you upgrade PHP.
Document the exit criterion now. Because this is a contain-don't-adopt dependency, make the migration target explicit (attributes on PHP 8.1+, or a maintained reflection library) and treat the wrapper as the migration boundary.
composer.json, vendor/fork if you must keep it, and audit before every PHP upgrade. Don't let it spread beyond the legacy boundary.ReflectionAttribute in the standard library does the same job with no dependency. This package adds nothing there.AnnotationRegistry::registerLoader('class_exists') (or register class names) before reading. Symptom: "the annotations are just being ignored" — expect a painful debugging session if you miss it.opcache.save_comments=0 strips docblocks. Every annotation silently disappears in environments where comments aren't retained. If it works locally and fails on a tuned prod box, check this first.use in the docblock often yields null instead of an exception. Assert on returned annotations during boot in dev, and log which classes were scanned.(new ReflectionClass($class))->getDocComment() first. Missing comment → upstream problem (opcache, formatter, wrong file). Comment present → autoloading or syntax inside the docblock. Only then suspect the parser.require or a PSR-4-compliant path.app(), no config(), no DI inside an annotation. Resolve dependencies in your consumer.doctrine/annotations is already present for other reasons, align the reader version with it, or you'll get two parsers and divergent behaviour.How can I help you explore Laravel packages today?