crodas/notoj
Notoj is a lightweight PHP annotation parser that reads docblock annotations with a fast tokenizer, no heavy dependencies. Define annotation classes extending Notoj's base, then parse methods, classes, and properties to build routing, validation, or metadata layers.
Install with Composer:
composer require crodas/notoj
There's no service provider or facade — this is a standalone parser you call directly. The two entry points you'll touch first are Notoj\File (single file) and Notoj\Dir (recursive directory scan):
use Notoj\Dir;
$dir = new Dir(base_path('app/Http/Controllers'));
foreach ($dir->getAnnotations() as $annotation) {
echo $annotation->getName(); // e.g. "Route"
var_dump($annotation->getArgs()); // parsed key/value arguments
echo $annotation->getObject()->getName(); // the class/method it was attached to
}
Look here first: the Annotation\Annotation value object (getName(), getArgs(), getObject()). Everything else is a scanner that produces these.
First real use case: you want lightweight, docblock-driven discovery — e.g. scanning app/ for @FeatureFlag("...") or @OnQueue("...") markers on controller methods, then registering them in a service provider or an Artisan command. Prototype it in a tinker session against a single controller file before wiring it into a provider; docblock parsing is fiddly and you want to see the raw output early.
Directory scan → filter by name. The common shape is a one-shot scan that filters annotations down to the ones you care about:
use Notoj\Dir;
$flags = collect((new Dir(app_path()))->getAnnotations())
->filter(fn ($a) => $a->getName() === 'FeatureFlag')
->map(fn ($a) => [
'target' => $a->getObject()->getName(),
'name' => $a->getArgs()['value'] ?? null,
]);
Wrap it in a resolver service, cache the result. Parsing every file on every request is wasteful. Put the scan behind a class, cache the normalized array (Cache::rememberForever, keyed by a hash of the file mtimes), and only re-parse when the cache is cold or cleared in CI/deploy.
Artisan command as the entry point. A php artisan annotations:list command that dumps parsed annotations is the fastest way to debug syntax and to give teammates visibility into what the scanner actually sees. Reuse the same service the provider uses.
Bind once, resolve everywhere. In a ServiceProvider::register(), run the scan and bind a registry object into the container ($this->app->singleton(AnnotationRegistry::class)) so controllers, middleware, and commands all read from the same parsed result rather than re-scanning.
Combine with Reflection for behavior. getObject() returns a reflection handle (ReflectionClass, ReflectionMethod, ReflectionProperty, ReflectionFunction depending on where the annotation sat). Use it when you need to instantiate, invoke, or read constructor parameters of the annotated target — e.g. auto-register routes or event listeners discovered by annotation.
Targeted scans over whole-app scans. Point Notoj\Dir at app/Http/Controllers or app/Models rather than app/ or the project root. Fewer files, fewer surprises from vendor docblocks and test fixtures, and a much faster cold cache build.
#[Attribute] + ReflectionAttribute is the built-in, editor-supported, static-analysis-friendly path. Use Notoj only when you're stuck on DocBlock annotations (legacy code, or you specifically want comments that don't affect reflection).@Foo vs @\App\Annotations\Foo vs an imported alias can resolve differently than you expect. Prefer fully-qualified names in docblocks, and always dd($annotation->getName()) during prototyping rather than guessing.?? null, filter_var) and treat getArgs() as untrusted input.doctrine/annotations) so a single composer update can't break discovery across the app./** ... */ — /* */ and // are silently ignored.getAnnotations() are the sanctioned way to add behavior. Prefer these over fork-and-patch; they survive Composer updates.How can I help you explore Laravel packages today?