crodas/class-info
crodas/class-info is a lightweight PHP introspection utility for reading class metadata via reflection. It exposes methods, properties, parent classes, interfaces, and docblocks, making it useful for documentation generation and runtime tooling that needs structured class information.
Install the package and confirm the API surface for the tagged version you pulled — the README is thin, so src/ is the real documentation.
composer require crodas/class-info
The package answers one question: what symbols does this PHP file declare? It does that by tokenizing the source (token_get_all) and never include-ing the file, so it is safe to point at untrusted or side-effect-heavy code such as vendor/.
Typical first use:
require __DIR__ . '/vendor/autoload.php';
$info = new ClassInfo\ClassInfo(__DIR__ . '/app/Support/helpers.php');
var_dump($info->getClasses()); // ['App\Support\Foo', ...]
var_dump($info->getFunctions()); // ['app_helper', ...]
If your tag exposes the helpers statically (ClassInfo::getClasses($file)), the result shape is the same — an array of fully-qualified names as written in the file, namespaces resolved, no autoloading performed.
Where to look first:
README.md for the constructor signature.src/ClassInfo.php (or the single class file) for the exact getter names — expect getClasses(), getFunctions(), and possibly getInterfaces() / getTraits().tests/ for edge cases the author actually supports (namespaces, conditional declarations, anonymous classes).First real use case in a Laravel app: build a class => file map over app/ so you can register plugins, discover Artisan commands, or write a lint rule asserting one class per file.
$map = [];
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(app_path()));
foreach ($it as $file) {
if ($file->getExtension() !== 'php') continue;
$info = new ClassInfo\ClassInfo($file->getPathname());
foreach ($info->getClasses() as $class) {
$map[$class] = $file->getPathname();
}
}
Cache the scan, key on mtime. Tokenizing a few thousand files on every request is the fastest way to make this package look slow. Persist the map to storage/app/classmap.json and only re-parse files whose filemtime() moved.
$cache = Cache::rememberForever('classmap', fn () => buildClassmap(app_path()));
// invalidate on deploy, or diff mtimes per file
Two-pass discovery: ClassInfo then Reflection. Use ClassInfo only to find names cheaply, then load metadata with new ReflectionClass($fqcn) for parents, interfaces, and attributes. Doing it the other way around means autoloading every file in the tree just to ask what's in it.
Convention linting in CI. A one-liner per file — count($info->getClasses()) === 1 — catches the "two classes in one file" mistake that silently breaks PSR-4 autoloading. Wire it into a Pest test rather than a custom Artisan command; it's ten lines and runs everywhere.
Procedural helper discovery. Laravel projects accumulate app/Support/helpers.php files. Use getFunctions() to register a map of function => file so a require only happens when the helper is actually called, or to assert that every helper is namespaced.
Plugin / module registration. Combine with a directory convention: scan app/Plugins/*/Plugin.php, read getClasses(), filter by an interface you check via Reflection, and register the survivors in a service provider. This avoids enumerating plugin classes by hand.
Fixture-driven development. Because behavior around traits, enums, anonymous classes, and multiple namespaces per file is version-dependent, keep a tests/fixtures/ folder with one pathological file per feature and assert the exact arrays. That fixture suite becomes the spec the README lacks.
Pair with Composer's ClassMapGenerator when you need standards compliance. Composer's --optimize-autoloader already produces a classmap and handles PSR-4 edge cases. Reach for ClassInfo when you need functions too, or when you want the map without a Composer run.
if (!class_exists(...)) will be reported even when it is never defined at runtime. Treat the output as "candidate symbols", not "loaded symbols".class_alias, no eval, no dynamic definitions. Anything created at runtime is invisible. Same for classes generated into strings.T_ENUM and attributes. Verify with a fixture before trusting getClasses() on a PHP 8.1+ codebase; if enums are missed, either pin the scan to class-like files or fork the token loop (it's short).getClasses(). Some versions return only T_CLASS. If you need them, call the dedicated getter or scan for T_INTERFACE / T_TRAIT yourself.[] rather than throwing. Check is_readable() yourself or you'll silently drop classes from the map.strtolower() when the map is only used for lookup, but keep the original casing for Reflection.use-statement resolution. You get the FQCN of the declaration, not the parent class or imported names. That's Reflection's job.composer.json constraints. If it fights your platform, the whole feature is roughly 80 lines of token_get_all — fork it, or swap to nikic/php-parser if you later need real AST work.How can I help you explore Laravel packages today?