illuminate/reflection
Illuminate\Reflection wraps PHP's native ReflectionClass with Laravel-specific helpers for inspecting methods, properties, traits, attributes, and doc comments. It underpins the service container, route model binding, and much of the framework's internal introspection.
illuminate/reflection is the Composer split of the small static helper class Illuminate\Support\Reflector from the Laravel framework. It ships no service provider, no facade, no config file, and nothing to publish — it's a handful of static methods that wrap PHP's native ReflectionParameter / ReflectionMethod APIs with Laravel-flavoured edge-case handling (nullable types, unions, self/parent, backed enums).
Before installing: in a normal laravel/framework app you already have this class — composer require illuminate/reflection is only relevant if you're pulling Laravel components standalone. Check first:
ls vendor/illuminate/reflection/src 2>/dev/null || grep -rn "class Reflector" vendor/illuminate/support
Look here first: the class source itself (Reflector.php, roughly 100 lines) and the call sites in illuminate/container (Container::getDependencies(), Container::resolveDependencies()) — that's the canonical example of how the methods are meant to be composed.
First use case: reproduce what the container does when it injects into a constructor. Write a fixture with a union-typed, a nullable, and a parent-typed parameter, then dump what the helper reports:
use Illuminate\Support\Reflector;
$params = (new ReflectionMethod(MyJob::class, '__construct'))->getParameters();
Reflector::getParameterClassName($params[0]); // 'App\Contracts\Logger' | null
Reflector::getParameterClassNames($params[0]); // union members, e.g. ['Foo', 'Bar']
Reflector::isParameterSubclassOf($params[0], App\Contracts\Logger::class);
Doing this once tells you more than any blog post — the public API has drifted across versions (see Gotchas).
1. Custom callable/DI resolvers. The main day-to-day use is any code that must "call this callable, injecting whatever it needs" outside the container's own call(): plugin/action registries, webhook handlers, listener dispatchers, test helpers. The loop is: iterate getParameters(), skip variadics, take getParameterClassName(), else $parameter->isDefaultValueAvailable() ? getDefaultValue() : $container->make(...), else throw with the parameter name. Using Reflector::getParameterClassName() instead of hand-rolling getType() instanceof ReflectionNamedType gets you correct self/parent resolution and builtin/null filtering for free.
2. Method-level injection. Same pattern one level down: ReflectionMethod + getParameterClassName() per parameter. This is how you'd build dynamic controller/middleware invocation, a micro "dispatch by convention" router, or attribute-driven job parameter binding.
3. Type-based filtering for discovery. isParameterSubclassOf() is a cheap guard before wiring a parameter to a service; isParameterBackedEnumWithStringBackingType() is the check you want when generating validation rules or form options from a method signature (that's why it exists — it backs Rule::enum-style code paths).
4. Callable checks. ::isCallable($class, $method) respects magic __call/__callStatic where method_exists() gives a false negative — useful in macro systems and hook registries.
5. Integrate via the container, not the split package. Depend on illuminate/container (or laravel/framework) in composer.json and let Reflector arrive transitively. Add illuminate/reflection explicitly only if you are genuinely framework-free; then pin it to the same major as any sibling Illuminate packages.
6. Cache the result. Reflection is cheap per call but not free in a request loop. For boot-time discovery (routes, commands, plugins) build a Class => [dependencies] map once in a service provider and write it to bootstrap/cache, mirroring what Laravel does with its package-manifest and event-discovery caches.
7. Test the resolution layer directly. Fixture classes covering self, parent, union, nullable-with-default, enum, and variadic parameters give you faster, more targeted coverage than booting the container in every test.
Reflector::make(), ::resolve(), or ::canBeInstantiated(). If a snippet shows those, it's wrong — that behaviour lives on Illuminate\Contracts\Container\Container (make, bound, getParameterOverride). Reflector is a static helper, not a facade and not a container proxy.getAttributes(), no docblocks, no getMethods() traversal. Use ReflectionClass/ReflectionMethod directly for that and reserve Reflector for parameter-type interpretation.getTypeHint), and removed/moved across majors. Trust grep -n "public static function" vendor/illuminate/reflection/src/Reflector.php, not tutorials — especially if you're on the split package rather than the framework, where the version you get is whatever Composer resolved.getParameterClassName() returns null for three different reasons — builtin type (int, array), untyped parameter, and non-named type. Log the ReflectionParameter itself when debugging, not just the null, or you'll chase the wrong cause.getParameterClassNames(). getParameterClassName() handles ReflectionNamedType only. And the plural variant does not resolve self/parent in every version — verify before relying on it in a generic resolver.A&B) are unhandled and surface as null. Fall back to native reflection for those.static is not self. getParameterClassName() resolves self and parent but returns the literal 'static' for static return/param types, which is not instantiable via the container. Guard it or you'll get a confusing BindingResolutionException.isParameterSubclassOf() returns false (never throws) for builtin or untyped parameters — fine in normal flow, misleading when you're debugging an injection failure. Assert on the parameter name alongside it.?Foo $foo) and an ambiguous union. Run Reflector::getParameterClassNames() on the constructor parameter — it shows exactly what the container saw — and confirm allowsNull() / isDefaultValueAvailable().illuminate/container. Don't pin your architecture to it, don't plan on upstream fixes, and if you're outside Laravel, copying the ~100-line Reflector.php into your own support namespace is lower risk than a long-lived Composer dependency.Reflector:: calls behind one small adapter class of your own — that's your swap point to native reflection if the split package stalls.How can I help you explore Laravel packages today?