Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Better Reflection Laravel Package

roave/better-reflection

Better Reflection is a PHP reflection API for static analysis: reflect on classes without loading them, parse classes from strings, inspect closures, and extract AST and type information. It prioritizes features over speed, so it is not intended for runtime use.

View on GitHub
Deep Wiki
Context7

Getting Started

Install it, then confirm you actually need it — the README is blunt that this replaces native reflection for features, not speed:

composer require --dev roave/better-reflection

Keep it in require-dev unless an Artisan command or package ships to production; most Laravel uses are developer/CI tooling.

Where to look first, in this order:

  1. docs/usage.md — explains SourceLocators and why reflectClass() may not "just work" in your app.
  2. docs/compatibility.md — the honest table of what is not supported (newInstance(), getClosure(), extension reflection). Read this before designing anything.
  3. docs/find-reflection-on-line.md — small helper that pays off immediately.
  4. UPGRADE.md — this library churns APIs across majors; don't copy blog posts from 5.x.

First use case — a one-liner in Tinker that native reflection can't do. Reflect on a class without loading it into your request, e.g. a class in a package you're auditing:

$class = (new Roave\BetterReflection\BetterReflection())
    ->reflector()
    ->reflectClass(App\Models\Order::class);

$class->getMethods();          // same shape as native ReflectionClass
$class->getDocComment();       // works on classes that were never autoloaded

Then graduate to the two things that actually justify the dependency in a Laravel app: reflecting a whole directory of classes you never instantiate, and reading signatures/types without booting the framework or triggering side effects.

Implementation Patterns

Bind one reflector as a singleton

Don't construct BetterReflection per call — the AST parsing and locator work is the expensive part. Register a named locator set per purpose:

use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Reflector\DefaultReflector;
use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\AutoloadSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator;

$this->app->singleton(DefaultReflector::class, function () {
    $better = new BetterReflection();
    $astLocator = $better->astLocator();

    return new DefaultReflector(
        new AggregateSourceLocator([
            new AutoloadSourceLocator($astLocator),                       // anything Composer can find
            new DirectoriesSourceLocator([app_path()], $astLocator),      // whole-tree scans
        ]),
        $astLocator,
    );
});

If you need ComposerSourceLocator, you need the ClassLoader instance — grab it from spl_autoload_functions() or re-require base_path('vendor/autoload.php'). In a Laravel app the default AutoloadSourceLocator is usually enough and needs no loader handle.

Enumerate app classes: path → FQCN → reflection

DirectoriesSourceLocator locates by name; it doesn't hand you a list. Walk the filesystem yourself and derive the FQCN from your PSR-4 map in composer.json:

foreach (File::allFiles(app_path('Jobs')) as $file) {
    $class = 'App\\Jobs\\' . str_replace(
        ['/', '.php'],
        ['\\', ''],
        Str::after($file->getRealPath(), app_path('Jobs') . DIRECTORY_SEPARATOR)
    );

    $ref = $reflector->reflectClass($class);

    if (! $ref->implementsInterface(ShouldQueue::class)) {
        $this->warn("{$class} is not queueable");
    }
}

This is the workhorse pattern: an Artisan command that enforces conventions across app/ (Jobs implement ShouldQueue, every Policy method has a matching model parameter, every FormRequest defines rules()).

Read attributes without instantiating them

Laravel 11 leans heavily on attributes (#[Scope], #[UseFactory], #[On] in Livewire). Extraction works; instantiation doesn't:

foreach ($ref->getAttributes() as $attribute) {
    $attribute->getName();     // 'App\Attributes\RequiresPermission'
    $attribute->getArguments(); // resolved values, incl. ::class constants
    $attribute->isRepeated();
}

Build your own tiny resolver mapping name → behaviour. Don't plan on newInstance().

StringSourceLocator is your unit-testing escape hatch

Testing a rule engine against fixture files is slow and brittle. Feed source as a string instead:

$locator = new StringSourceLocator('<?php namespace Fixtures; class Foo {}', $astLocator);
$ref = (new DefaultReflector($locator, $astLocator))->reflectClass('Fixtures\Foo');

Same trick is useful at runtime for analysing generated/stub code (migration stubs, published package config classes, a "paste a class and inspect it" internal tool).

FindReflectionOnLine for debug and error tooling

$finder = (new BetterReflection())->findReflectionsOnLine();
$finder('/app/Http/Controllers/OrderController.php', 42);
// → ReflectionClass | ReflectionMethod | ReflectionFunction | null

Great for a custom exception handler / Ignition-style panel that turns a stack frame into "you are inside OrderController::store(), here's its declared return type and docblock" without loading the frame's class.

Cache aggressively, and namespace by deploy

Parsing thousands of files is slow; the reflector accepts a PSR-16 cache (Laravel's cache()->store() implements PSR-16), so plug it in and make sure the cache is flushed on deploy — otherwise CI sees yesterday's AST. Namespace the cache by git SHA for safety.

Gotchas and Tips

  • Never in the request path. The README states performance is much worse than native reflection. Artisan commands, queue jobs, CI, static analysis — fine. Middleware, model boot(), service container hot paths — no. If native ReflectionClass can do it at runtime on an already-loaded class, use native.
  • newInstance() is unsupported across ReflectionClass and ReflectionAttribute. You cannot construct an attribute instance or a reflected class. getClosure(), invoke(), getStaticVariables(), getClosureThis() are also missing or partial.
  • No extension/ReflectionExtension support, and getExtension() is not implemented. isInternal() and getExtensionName() work; don't try to introspect PHP internals.
  • Functions cannot be autoloaded. reflectFunction('my_helper') won't find your app/helpers.php. Functions are only reachable via a StringSourceLocator, SingleFileSourceLocator, or the line finder.
  • Autoload assumptions bite. The default locator assumes one class per file and a working PSR-4 autoloader. Classes declared alongside others, or in files Composer can't map, need an explicit locator. Aliased classes and anonymous classes can't be looked up by name.
  • PHP support trails the interpreter, and "supported" ≠ "complete". As of 6.75.0 you can install on PHP 8.6 (6.74.0 made the parser not choke on 8.6 syntax; 6.75.0 lifted the Composer constraint). Treat that as "runs and parses", not "every 8.6 reflection surface is implemented" — docs/compatibility.md is still the source of truth per version. If you're on a PHP pre-release, pin BetterReflection and exercise your scanning command in CI before trusting the output.
  • Composer conflict with php-parser. BetterReflection pins nikic/php-parser, which Pint, PHPStan, Rector, and several Pest plugins also depend on. Run composer why nikic/php-parser before upgrading, and check the installed major's PHP floor against your app's support matrix.
  • API churn across majors. AST access in particular has moved (the old getAst() on methods is not guaranteed on 6.x — go through the configured AstLocator). Always consult UPGRADE.md and the version's own docs when you touch AST code.
  • Trait and inherited members. getMethods() merges trait and parent methods; getDeclaringClass() may hand you the trait. If you're generating docs or enforcing signatures, decide explicitly whether inherited members count. If you enforce visibility rules (ReflectionProperty::isReadable() / isWritable()) on trait-composed properties, note 6.75.0 fixes those returning wrong results for trait scope — upgrade if you target 6.74.x in that kind of tooling.
  • Default values are best-effort. getDefaultValue() on parameters "makes some assumptions" — fine for docs, risky for codegen that must reproduce exact source.
  • Memory on large scans. Reflecting every class in a big app is heavy. Chunk the file list, drop references between batches, and prefer a single command invocation over a loop of Artisan::call().
  • Debugging tip: when a reflection comes back wrong (wrong file, missing method), dump $ref->getFileName() and getStartLine(). Nine times out of ten the locator resolved a different file than you assumed, not a library bug.
  • Extension point: if none of the bundled locators fit (database-stored code, eval'd classes, encrypted vendor code), implement SourceLocator yourself and compose it into an AggregateSourceLocator — that's the sanctioned way to teach the library where your code lives.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style