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.
What the package actually is. Better Reflection is a source-code reflection engine: it parses PHP files with nikic/php-parser and exposes a ReflectionClass-shaped API over the AST, without loading or executing the code. It is explicitly documented as not for runtime usage — the README states performance is "much worse" than native reflection and that the "better" refers to features, not speed. That single sentence determines the entire architectural placement of this dependency. Nothing in release 6.75.0 changes this positioning.
Architecture fit. Good fit as a development and CI-time capability, poor fit anywhere else. In a Laravel codebase the realistic placements are:
| Placement | Fit | Rationale |
|---|---|---|
| CI job enforcing architecture/convention rules | Strong | Deterministic, offline, no app boot required |
php artisan diagnostics command (local/CI only) |
Strong | Can inspect classes that would fatal on load (missing extensions, env-specific deps) |
| Code/scaffold generation from strings or templates | Moderate | StringSourceLocator is genuinely differentiating; but generation must be text/AST-based since instantiation APIs are absent |
| Package or library authoring (analyse own API surface) | Moderate | Useful for generating API docs and BC-break detection across releases |
| Request path, queue worker, middleware, service provider boot | Unfit | Order-of-magnitude slower than native reflection; adds a parser to the hot path |
Replacing app()->make() / container introspection |
Unfit | Static analysis cannot see container bindings, facade resolution, or Eloquent magic |
Integration feasibility: high. MIT licensed, Composer-installable as a dev dependency, no services or infrastructure. Laravel's PSR-4 autoloading maps cleanly onto AutoloadSourceLocator, and the SourceLocator abstraction (ComposerSourceLocator, SingleFileSourceLocator, StringSourceLocator, directory/aggregate variants) makes scope explicit and reproducible — which is exactly what a CI gate needs.
Technical risk. The risk profile is dependency and drift risk, not runtime risk. 6.75.0 materially improves one line of it and leaves the rest unchanged:
php-parser version coupling. This remains the sharpest practical risk, and 6.75.0 does not address it. PHPStan, Rector, PHP-CS-Fixer, and this library all constrain nikic/php-parser. In a single composer.json, conflicting constraints are a common cause of un-installable dev dependencies. Plan for isolation (see Integration Approach).#1593 explicitly allows installation on PHP 8.6. Previously an open question was whether supporting a future PHP minor would force a BetterReflection major. This release demonstrates the project widens the runtime constraint ahead of the PHP release within the v6 line, so PHP-version-driven major upgrades are less likely than feared. The corollary: version constraints should be ^6.75, not a narrower ~6.7x, so these widening releases are absorbed automatically.(new BetterReflection())->reflector() returning DefaultReflector). Major upgrades track PHP releases and come with a documented UPGRADE.md — budgeted work, not free. 6.75.0 is a minor and requires no code changes.#1591 corrects ReflectionProperty::isReadable() / isWritable() for trait scope. Practical implication: any rule asserting property visibility/readability on classes that consume traits was producing wrong answers on earlier 6.x versions. If a rule of that shape exists (or is planned), re-baseline it against 6.75.0 — prior violation counts on those rules are not trustworthy. This is a silent-false-negative class of bug, which is exactly the failure mode that erodes trust in a bespoke gate.newInstance*, ReflectionAttribute::newInstance(), getExtension(), and getClosureThis/Scope as unsupported. Consequences unchanged: attribute-driven analysis (routes, validation, authorization) yields names and argument AST only — you must interpret arguments yourself; extension-provided classes (Redis, Imagick, custom C extensions) are opaque; and global functions cannot be autoloaded, so app/helpers.php-style files and vendor files autoloads need an explicit source locator or they are invisible.__call/__get proxies, macros, container resolution, model attribute casting, Blade, and Livewire hydration are all invisible. A TPM must set expectations that rules can only assert on declared structure.dependents: 0 is a data artifact. This library is a transitive dependency of widely used PHP tooling; real-world exposure is far higher than the metric suggests. Conversely, ~1.2k stars understates its maintenance quality — mutation-testing badge (now exercised on PHP 8.5 per #1592), type-coverage gate, docs, and an upgrade guide are strong maintenance signals.Key questions before committing:
^6.75 and confirm our target PHP matrix is inside the supported set.vendor/ and internal/extension classes, or only app/? (The latter removes most of the coverage gaps.)composer.json acceptable to the team, or must everything live in the app's dev dependencies?#1591 fix), and do we need to re-baseline any measurements taken on a pre-6.75.0 version?Stack fit. Treat this as a tooling library, not a Laravel library. Recommended comparison before adoption:
| Need | Existing option | Where Better Reflection adds value |
|---|---|---|
| Type/flow analysis | Larastan / PHPStan | Nothing — do not duplicate; PHPStan already ships its own reflection layer |
| Automated refactors | Rector | Nothing |
| Architecture/dependency rules | Arkitect-style tools | Only if we need AST bodies or unloaded-class inspection |
| "Reflect on a class without loading it" | none | This is the unique slot — e.g. auditing classes that fatal on load, or analysing code from strings |
If the answer to "what are we building?" is a convention and architecture CI gate, Better Reflection is a reasonable engine. If the answer is "better static analysis," buy a tool rather than build one.
Isolation and dependency hygiene. Install with composer require --dev roave/better-reflection:^6.75 in the application only if php-parser constraints resolve against the existing static-analysis stack. Otherwise, create a dedicated tooling project (tools/architecture/composer.json) invoked by CI, or use bamarni/composer-bin-plugin to give the tool its own namespace and lockfile. Isolating avoids the most likely failure mode: a Rector or PHP-CS-Fixer upgrade becoming blocked by, or blocking, this package. Use a caret constraint so per-minor releases like 6.75.0 (PHP 8.6 widening, bugfixes) arrive unattended; gate majors behind manual review.
Wiring in a Laravel app (if not isolated):
ToolingServiceProvider registered conditionally — $this->app->environment(['local', 'testing']) or an explicit config('tooling.enabled') flag. Never register unconditionally.AggregateSourceLocator over explicit directories (app/, src/, packages/*/src) plus ComposerSourceLocator only for the vendor paths genuinely required. Prefer directory locators over AutoloadSourceLocator for CI determinism — the autoload technique assumes one class per file and a non-authoritative autoloader, both of which break under --classmap-authoritative or generated/compiled production autoloads.config/tooling.php with paths, exclusions, baseline/exemption list, and cache settings so rules are data, not hardcoded.php artisan arch:check) that returns a non-zero exit code on violation. This is the entire developer-facing surface.Adapter layer. Wrap the package behind a small internal interface (DeclarationInspector or similar) exposing only what rules need — class name, methods, parameter types, return types, docblock, AST, and property visibility. Two benefits: rules never import Roave\ namespaces, so a major upgrade touches one file; and the same interface can be backed by native reflection later if the package is dropped. Because isReadable()/isWritable() semantics changed in 6.75.0, keep any visibility assertions behind this adapter rather than open-coded in rules.
Compatibility. 6.75.0 is a minor feature release with no deprecations or BC breaks recorded; no adapter or rule changes are required to move from any 6.7x version. The only behavioural delta to watch is the trait-scope property visibility fix — it is a correctness improvement, so expect it to surface previously-unreported violations if such rules exist. Treat that as a one-time re-baseline event, not a regression.
Sequencing.
app/; reflect 100–200 representative classes including Eloquent models, jobs, form requests, enums, and at least one trait-heavy class (to exercise the #1591 path). Measure wall time, peak memory, and failure count. Confirm composer constraint resolution against the existing dev stack on the newest supported PHP. Go/no-go here.ShouldQueue", "no app/Models class depends on App\Http"). Report-only in CI, published to PR comments.UPGRADE.md path), and a quarterly review of which rules still earn their keep.If a spike was already run on a pre-6.75.0 version, re-run only the visibility-sensitive rules and the PHP-version matrix check — the rest of the measurement stands.
Maintenance. Low steady-state cost if the adapter holds. Two recurring obligations: (a) upgrading in lockstep with new PHP minor/major releases, following UPGRADE.md; (b) reconciling php-parser constraints whenever the broader static-analysis stack moves. 6.75.0 is a good demonstration of the cadence working in our favour: PHP 8.6 support arrived as a minor, meaning (a) is mostly automated. The dangerous case remains an unpinned version floating to a major that changes Reflector construction. Pin to ^6.75 with Dependabot auto-merge for minors/patches and a manual-merge policy for majors.
Support. Open source, MIT, no SLA. Maintenance quality is above average for the ecosystem: mutation testing is actively exercised across PHP versions (#1592 now covers 8.5), there is a type-coverage gate, a documented upgrade path, and releases ship roughly monthly with small, reviewable PRs. The vendor offers commercial consulting if a blocker appears. One caveat visible in this release: all three PRs came from a single contributor. That is normal for a small maintainer group and not a red flag, but it is a bus-factor consideration — do not assume a same-week turnaround for a bespoke blocker. Practically: the support model is internal ownership. A rule nobody owns becomes a build-breaking liability within two quarters. Name an owner and a code-review path for additions.
Scaling. Cost is files × analyses, not traffic. A full scan of a 1–2k class application will dominate a small CI budget without caching; with a warm cache and directory scoping it drops to negligible for unchanged code. Design for file-by-file streaming — do not accumulate all ReflectionClass objects in a collection, since AST retention is the primary memory risk. If rule count grows, split rules into parallel CI jobs by concern rather than adding workers to one job.
Failure modes and mitigations.
| Failure mode | Symptom | Mitigation |
|---|---|---|
| Unparsable file (syntax newer than bundled parser) | Exception on scan | Fail closed; pin library ahead of PHP upgrade (6.75.0 now covers PHP 8.6) |
| Class not found in locator | IdentifierNotFound / silently absent |
Aggregate locator + explicit assertion that expected classes resolve; fail on empty result set |
| Authoritative/optimized autoloader | Wrong or empty reflection | Use directory locators, not AutoloadSourceLocator, in CI |
Global/vendor functions (helpers.php) invisible |
Rules quietly skip | Explicit SingleFileSourceLocator/directory locator for those files |
| Stale cache | False negatives after refactor | Key on content hash; invalidate on branch change |
| Rule crashes | Rule reports zero violations | Fail closed; assert expected class count per run |
php-parser constraint conflict |
composer update blocked |
Isolated tooling project or bin-plugin |
| Memory exhaustion on deep inheritance | OOM kill in CI | Streaming iteration, memory_limit headroom, chunked runs |
| Attribute arguments uninterpretable | Rules can't read #[Route] semantics |
Parse argument AST directly; document the limitation to rule authors |
| Property visibility misreported in trait scope | Violations missing or spurious pre-6.75.0 | Upgrade to ^6.75, re-baseline visibility rules once, keep assertions behind the adapter |
Ramp-up. A mid-level PHP engineer can use reflectClass() / getMethods() / getReturnType() productively in 2–3 days. Writing trustworthy rules is the real cost: handling traits, interface inheritance, promoted constructor properties, enums, docblock generics, and conditional declarations is where the 3–6 weeks go for a first meaningful ruleset. The trait-scope visibility fix in 6.75.0 is a reminder that this ramp-up includes validating the reflection layer's own edge cases, not just writing rule logic. Budget developer time, not licence cost (which is zero). Include a short internal write-up of the adapter API and the false-positive policy; the most common failure of bespoke architecture tooling is abandonment after the first wave of noisy violations.
Verdict. Approve as a scoped, dev-and-CI-only tooling dependency, unchanged from the prior assessment and now slightly de-risked: 6.75.0 raises the PHP ceiling inside v6 and fixes a correctness bug that would otherwise have silently undermined visibility rules. Adopt at ^6.75. Reject outright if the intended use is runtime reflection, container introspection, or duplicating capabilities already provided by Larastan/PHPStan. The package is well maintained and technically strong at exactly one thing — reflecting on code that is not loaded — and the value case must be anchored to that capability.
How can I help you explore Laravel packages today?